v0.5.0 AI Drafted AI Validated

Okta Hardening Guide

Identity Last updated: 2026-09-24

Identity Provider hardening for SSO, MFA policies, and API token security

View:

Overview

Okta is an identity and access management (IAM) platform that controls authentication for 18,000+ organizations with 7,000+ integrations in its network. As the central authentication provider for enterprise applications, Okta represents the highest-leverage hardening target in most organizations. The 2022 LAPSUS$ breach and October 2023 support system breach (files associated with 134 customers accessed, including HAR files carrying session tokens) demonstrated how stolen session tokens grant attackers SSO access to thousands of downstream applications.

Intended Audience

  • Security engineers managing identity infrastructure
  • IT administrators configuring Okta tenants
  • GRC professionals assessing IAM compliance
  • Third-party risk managers evaluating SSO integrations

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 Okta-specific security configurations including authentication policies, OAuth/SCIM governance, session management, and integration security. Infrastructure hardening for Okta agents is out of scope.


Table of Contents

  1. Authentication & Access Controls
  2. Network Access Controls
  3. OAuth & Integration Security
  4. Session Management
  5. Monitoring & Detection
  6. Third-Party Integration Security
  7. Operational Security
  8. Compliance Quick Reference

1. Authentication & Access Controls

1.1 Enforce Phishing-Resistant MFA (FIDO2/WebAuthn)

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.3, 6.5
NIST 800-53 IA-2(1), IA-2(6)
DISA STIG V-273190, V-273191, V-273193 (HIGH), V-273194 (HIGH)

Description

Require phishing-resistant authenticators (FIDO2 security keys or platform authenticators) for all users, especially administrators. This eliminates vulnerabilities to real-time phishing proxies that bypass TOTP and push-based MFA.

Rationale

Why This Matters:

  • TOTP and push notifications can be intercepted via real-time phishing (Evilginx, Modlishka)
  • The October 2023 Okta breach was enabled by session cookie theft from HAR files
  • FIDO2 binds authentication to specific origins, preventing token theft

Attack Prevented: Real-time phishing, session hijacking, MFA bypass

Real-World Incidents:

  • October 2023 Okta Support Breach: A threat actor accessed support-case files associated with 134 customers, including HAR files containing session tokens
  • January 2022 LAPSUS$ Breach: Third-party support engineer compromised via social engineering

Prerequisites

  • Okta tenant with MFA capabilities
  • FIDO2-compatible security keys (YubiKey 5 series, Google Titan)
  • Super Admin access for policy configuration
  • User inventory for phased rollout

ClickOps Implementation

Step 1: Enable Passkey (FIDO2 WebAuthn) as an Authenticator

  1. Navigate to: Security → Authenticators
  2. Click Add authenticator → select Passkey (FIDO2 WebAuthn) → Add. If it is already listed, click Actions next to it → Edit, then Edit on its settings page
  3. Configure:
    • User verification: Required for both Enrollment and Authentication
    • Block synced passkeys: enable when policy requires device-bound keys (L3 / NIST AAL3)
    • Required characteristics: Hardware protection (add FIPS compliant in FIPS environments; see 1.8)
  4. Click Save

Step 2: Create a Phishing-Resistant Authentication Policy

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. Click Create policy → Name: “Phishing-Resistant MFA” → Create policy
  3. Click Add rule and configure:
    • IF User’s group membership includes: At least one of the following groups → your administrators group
    • THEN Access is: Allowed after successful authentication
    • User must authenticate with: Any 2 factor types
    • Possession factor constraints are: Phishing resistant (add Hardware protected for L3)
    • Prompt for authentication: Every time user signs in to resource
  4. Click Save, then assign the applications this policy should protect (the policy’s Application tab)

Step 3: Require Phishing-Resistant Factors on the Okta Dashboard and Admin Console The Global Session Policy cannot target the Admin Console or require a specific authenticator, so enforce this in the app sign-in policies of both Okta apps:

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. Click the Okta Dashboard policy
  3. Click Actions next to the top rule → Edit
  4. In “User must authenticate with”, select Password + Another factor or Any 2 factor types
  5. In “Possession factor constraints are”, check Phishing resistant
  6. Repeat for the Okta Admin Console policy
Specification Requirement
DISA STIG V-273190, V-273191 Phishing resistant box must be checked for Dashboard and Admin Console
DISA STIG V-273193, V-273194 (HIGH) MFA required: “Password/IdP + Another factor” or “Any 2 factor types”

Time to Complete: ~30 minutes (policy) + user enrollment time

Code Implementation

Code Pack: Terraform
hth-okta-1.01-enforce-phishing-resistant-mfa.tf View source on GitHub ↗
# Enable FIDO2 (WebAuthn) as an authenticator
resource "okta_authenticator" "fido2" {
  name   = "FIDO2 WebAuthn"
  key    = "webauthn"
  status = "ACTIVE"
  settings = jsonencode({
    userVerification = "REQUIRED"
    attachment       = "ANY"
  })
}

# Signon policy requiring phishing-resistant MFA for admins
resource "okta_policy_signon" "phishing_resistant" {
  name        = "Phishing-Resistant MFA Policy"
  status      = "ACTIVE"
  description = "Requires FIDO2 for all admin access"
  priority    = 1

  groups_included = [var.admin_group_id]
}

# Rule enforcing FIDO2 on the phishing-resistant policy
resource "okta_policy_rule_signon" "require_fido2" {
  policy_id          = okta_policy_signon.phishing_resistant.id
  name               = "Require FIDO2"
  status             = "ACTIVE"
  priority           = 1
  access             = "ALLOW"
  mfa_required       = true
  mfa_prompt         = "ALWAYS"
  primary_factor     = "PASSWORD_IDP_ANY_FACTOR"
  session_lifetime   = 120
  session_persistent = false
}
Code Pack: API Script
hth-okta-1.01-enforce-phishing-resistant-mfa.sh View source on GitHub ↗
# Create an app sign-in policy (ACCESS_POLICY takes no policy-level conditions)
info "1.1 Creating phishing-resistant MFA policy..."
POLICY_ID=$(okta_post "/api/v1/policies" '{
  "type": "ACCESS_POLICY",
  "name": "Phishing-Resistant MFA Policy",
  "description": "Requires a phishing-resistant possession factor",
  "status": "ACTIVE"
}' | jq -r '.id // empty')
# Rule: two factors, one of them phishing resistant, re-verified at every sign-in
info "1.1 Creating policy rule requiring a phishing-resistant factor..."
RULE_BODY=$(jq -n --argjson conditions "${GROUP_CONDITION}" '(if $conditions == {} then {} else {conditions: $conditions} end) + {
  type: "ACCESS_POLICY",
  name: "Require phishing-resistant MFA",
  priority: 1,
  actions: {
    appSignOn: {
      access: "ALLOW",
      verificationMethod: {
        type: "ASSURANCE",
        factorMode: "2FA",
        reauthenticateIn: "PT0S",
        constraints: [{possession: {phishingResistant: "REQUIRED"}}]
      }
    }
  }
}')
okta_post "/api/v1/policies/${POLICY_ID}/rules" "${RULE_BODY}" > /dev/null
Code Pack: Sigma Detection Rule
hth-okta-1.01-enforce-phishing-resistant-mfa.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'user.authentication.auth_via_mfa'
        debugContext.debugData.factor: 'FIDO2_WEBAUTHN'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - client.ipAddress
    - debugContext.debugData.factor
    - outcome.result
    - published

Validation & Testing

  1. Attempt admin login with only password - should be blocked
  2. Attempt admin login with TOTP - should be blocked (if FIDO2 required)
  3. Complete admin login with FIDO2 key - should succeed
  4. Review System Log for successful WebAuthn authentications

Expected result: Only FIDO2-authenticated sessions can access admin console

Monitoring & Maintenance

Ongoing monitoring:

  • Alert on authentication attempts that fail FIDO2 requirement
  • Monitor for users bypassing policy via legacy sessions

Detection rule: See the Sigma rule in Code Pack section 1.1 above.

Maintenance schedule:

  • Monthly: Review FIDO2 enrollment completion rates
  • Quarterly: Audit policy exceptions and temporary bypasses
  • Annually: Review authenticator hardware lifecycle (key expiration)

Operational Impact

Aspect Impact Level Details
User Experience Medium Users must carry/use security keys
System Performance None No performance impact
Maintenance Burden Medium Key distribution and replacement
Rollback Difficulty Easy Can disable policy rule

Potential Issues:

  • Lost security keys require backup authentication method
  • Platform authenticators may not work on shared devices

Rollback Procedure:

  1. Navigate to Authentication Policy
  2. Disable or lower priority of FIDO2 requirement rule
  3. Enable fallback MFA methods temporarily

1.2 Implement Admin Role Separation

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4, 6.8
NIST 800-53 AC-5, AC-6(1)

Description

Separate administrative privileges using Okta’s custom admin roles instead of granting Super Admin access. Create role-specific permissions for Help Desk and Application Admins, and give auditors Okta’s built-in Read-only Administrator role.

Rationale

Why This Matters:

  • Super Admin compromise provides complete tenant control
  • LAPSUS$ attack leveraged over-privileged support access
  • Least privilege limits blast radius of compromised accounts

Attack Prevented: Privilege escalation, lateral movement via admin accounts

ClickOps Implementation

Step 1: Create Custom Admin Roles

  1. Navigate to: Security → Administrators → Roles
  2. Click Create new role (Okta: Create a role), enter a Role name and Role description, choose permissions under Select permissions, and click Save role
  3. Create the following roles, using the permission names as the picker labels them (Okta: Role permissions):

Help Desk Admin (custom role):

  • User: View users and their details, Reset users’ passwords, Unlock users
  • Do not grant: Manage users, Manage policies, Manage applications, Manage API tokens

Application Admin (custom role):

  • Application: View application and their details, plus Manage applications (view, create, edit and delete apps; it also grants the Manage directories permission). Grant only Manage application general settings instead when the admin should change nothing beyond an app’s general settings
  • Do not grant: Manage policies, Manage identity providers, or any User permission beyond View users and their details

Security Auditor (read-only):

  • Do not build a custom role for this. The custom-role picker has no System Log or report permission, so a custom role cannot give an auditor log access
  • Assign the Standard Read-only Administrator role instead (“View most data in the Admin Console”). It can view users, groups, apps, reports, Okta settings, sign-on policies and the System Log, and it cannot edit data (Okta: Read-only administrators)
  • For auditors who need only logs and reports, the Standard Report Administrator role (“View all reports and the System log”) is narrower

Step 2: Assign Roles to Specific Groups

  1. Navigate to: Security → Administrators
  2. Click Add administrator
  3. Under Select admin, choose the user, group or app, then click Add assignment
  4. Choose the Role: one of the custom roles above, or the Standard Read-only Administrator for auditors
  5. For a custom role, also choose the Resource set (apps/groups) that scopes it — custom roles are always assigned together with a resource set — then click Save Changes

Code Implementation

Code Pack: API Script
hth-okta-1.02-admin-role-separation.sh View source on GitHub ↗
# Create custom Help Desk Admin role
info "1.2 Creating Help Desk Admin custom role..."
if okta_post "/api/v1/iam/roles" '{
  "label": "Help Desk Admin",
  "description": "Limited admin for password resets and account unlocks",
  "permissions": [
    "okta.users.read",
    "okta.users.credentials.resetPassword",
    "okta.users.lifecycle.unlock"
  ]
}' > /dev/null; then
  pass "1.2 Help Desk Admin role created"
  increment_applied
else
  fail "1.2 Failed to create Help Desk Admin role"
  increment_failed
fi

1.3 Enable Hardware-Bound Session Tokens

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 SC-23, IA-11

Description

Configure Okta to bind session tokens to specific devices using device trust and Okta FastPass, preventing session token theft and replay attacks.

Rationale

Why This Matters:

  • The October 2023 breach exploited stolen session cookies from HAR files
  • Device-bound tokens cannot be replayed from different devices
  • Okta FastPass provides passwordless + phishing-resistant authentication

Attack Prevented: Session token theft and replay from attacker-controlled devices (HAR-file session hijacking)

Real-World Incidents:

  • October 2023: Attackers exfiltrated HAR files containing session tokens from Okta support portal

ClickOps Implementation

Step 1: Enable Okta Verify with FastPass

  1. Navigate to: Security → Authenticators
  2. Click Actions next to Okta Verify → Edit
  3. Configure:
    • Verification options → User can verify with: check Okta FastPass
    • Device passcode or biometric user verification → Enrollment: Required (user verification during authentication is enforced by app sign-in policy rules, per the form)
  4. Click Save

Step 2: Configure Device Trust

  1. Navigate to: Security → Device Integrations → Endpoint management → Add platform
  2. Select the platform (iOS, Android, or Desktop (Windows and macOS only)) and connect the device management tool that manages it (for example Jamf Pro, Microsoft Intune, or VMware Workspace ONE)
  3. Create the device assurance policies that define a trusted device (see 1.12)

Step 3: Create a Device-Bound Sign-In Rule

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. Open the policy protecting the application, then Add rule:
    • IF Device state is: Registered
    • AND Device assurance policy is: the policy from 1.12
    • THEN Access is: Allowed after successful authentication
  3. Add a lower-priority rule for the same apps whose Access is: Denied, so unregistered or non-compliant devices cannot sign in

Code Implementation

Code Pack: API Script
hth-okta-1.03-enable-hardware-bound-session-tokens.sh View source on GitHub ↗
# Okta Verify must be active and require user verification (biometric or PIN)
OKTA_VERIFY=$(okta_get "/api/v1/authenticators" | jq -c '[.[] | select(.key == "okta_verify")][0] // {}')
OV_STATUS=$(printf '%s' "${OKTA_VERIFY}" | jq -r '.status // "ABSENT"')
OV_UV=$(printf '%s' "${OKTA_VERIFY}" | jq -r '.settings.userVerification // "unset"')
# App sign-in rules that allow access only from a registered/managed/assured device
DEVICE_RULES=0
for POLICY_ID in $(okta_get "/api/v1/policies?type=ACCESS_POLICY" | jq -r '.[].id'); do
  COUNT=$(okta_get "/api/v1/policies/${POLICY_ID}/rules" | jq '[.[]
    | select(.status == "ACTIVE" and .actions.appSignOn.access == "ALLOW")
    | select(.conditions.device.registered == true
             or ((.conditions.device.assurance.include // []) | length > 0))] | length')
  DEVICE_RULES=$((DEVICE_RULES + COUNT))
done

1.4 Configure Password Policy

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 IA-5(1)
DISA STIG V-273195, V-273196, V-273197, V-273198, V-273199, V-273200, V-273201, V-273208, V-273209

Description

Configure comprehensive password policies with appropriate complexity, age, and history requirements. These controls protect against weak passwords, password reuse, and rapid password cycling.

Rationale

Why This Matters:

  • Short, weak, or common passwords are cracked quickly by offline brute-force and dictionary attacks, handing attackers a foothold into the identity provider that fronts every connected application
  • Without password history and a minimum-age rule, users immediately cycle back to a favorite password after a forced reset, defeating rotation entirely
  • The common-password check rejects credentials already exposed in public breach corpuses, which are the seed lists for credential-stuffing campaigns
  • Because Okta is the SSO gateway, a single guessed password can cascade into access across thousands of downstream apps

Attack Prevented: Password brute-forcing, dictionary attacks, credential stuffing, password reuse

Prerequisites

  • Super Admin access
  • Okta-mastered users (not applicable if using external directory services)

Specification Requirements

Requirement L1 (Crawl) L2/L3 (DISA STIG)
Minimum length 12 characters 15 characters
Uppercase required Yes Yes
Lowercase required Yes Yes
Number required Yes Yes
Special character required Yes Yes
Minimum password age — 24 hours
Maximum password age 90 days 60 days
Common password check Recommended Required
Password history 4 generations 5 generations

ClickOps Implementation

Step 1: Access Password Authenticator Settings

  1. Navigate to: Security → Authenticators
  2. Click the Actions button next to Password
  3. Select Edit

Step 2: Configure Each Password Policy For each listed Password Policy, click Edit and configure:

Complexity Requirements:

  • Minimum Length: Set to at least 15 characters (L2/L3) or 12 (L1)
  • Upper case letter: ☑ Checked
  • Lower case letter: ☑ Checked
  • Number (0-9): ☑ Checked
  • Symbol (e.g., !@#$%^&*): ☑ Checked

Password Age Settings:

  • Minimum password age is XX hours: Set to at least 24 (prevents rapid cycling)
  • Password expires after XX days: Set to 60 (L2/L3) or 90 (L1)

Password History:

  • Enforce password history for last XX passwords: Set to 5

Step 3: Enable Common Password Check

  1. In the same policy’s Password Requirements section, under Common password check, check Restrict use of common passwords
  2. Click Update Policy

Note: Okta HealthInsight flags a password policy as weak below a minimum length of 12 and a password history of 24 (see 5.6). The history values in the table above meet DISA STIG; raise them toward 24 if you also want HealthInsight to pass this task.

Code Implementation

Code Pack: API Script
hth-okta-1.04-configure-password-policy.sh View source on GitHub ↗
CURRENT=$(okta_get "/api/v1/policies/${POLICY_ID}")
UPDATED=$(printf '%s' "${CURRENT}" | jq \
  --argjson len "${MIN_LENGTH}" --argjson maxage "${MAX_AGE_DAYS}" \
  --argjson minage "${MIN_AGE_MINUTES}" --argjson hist "${HISTORY_COUNT}" '
  .settings.password.complexity.minLength = $len
  | .settings.password.complexity.minLowerCase = 1
  | .settings.password.complexity.minUpperCase = 1
  | .settings.password.complexity.minNumber = 1
  | .settings.password.complexity.minSymbol = 1
  | .settings.password.complexity.dictionary.common.exclude = true
  | .settings.password.age.maxAgeDays = $maxage
  | .settings.password.age.minAgeMinutes = $minage
  | .settings.password.age.historyCount = $hist')
if okta_put "/api/v1/policies/${POLICY_ID}" "${UPDATED}" > /dev/null; then
  updated=$((updated + 1))
else
  fail "1.4 Failed to update policy '${POLICY_NAME}'"
  failed=$((failed + 1))
fi

Validation

  1. Navigate to: Security → Authenticators → Password → Edit
  2. For each policy, verify all settings match the requirements table above

Note: If Okta relies on external directory services for user sourcing, password policy is managed by the connected directory service.


1.5 Configure Account Lockout

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 AC-7
DISA STIG V-273189

Description

Enforce account lockout after consecutive invalid login attempts to protect against brute-force password attacks. This control significantly reduces the risk of unauthorized access via password guessing.

Rationale

Why This Matters:

  • Without a lockout threshold, an attacker can submit unlimited password guesses against an account until one succeeds
  • Automated brute-force and password-spray tools depend on being able to try many attempts per account undeterred
  • Locking accounts after a small number of failures forces attackers into slow, noisy attempts that are easy to detect
  • As the front door to SSO, an unlimited-guess Okta login exposes every federated application behind it

Attack Prevented: Brute-force password guessing, password spraying, credential stuffing

Specification Requirements

Requirement L1 (Crawl) L2/L3 (DISA STIG)
Lockout threshold 5 attempts 3 attempts
Lockout duration 30 minutes Until admin unlock

ClickOps Implementation

Step 1: Configure Password Authenticator Lockout

  1. Navigate to: Security → Authenticators
  2. Click the Actions button next to Password
  3. Select Edit

Step 2: Configure Each Password Policy For each listed Password Policy:

  1. Click Edit on the policy
  2. Locate the Lock out section
  3. Check Lock out user after X unsuccessful attempts
  4. Set the value to 3 (L2/L3) or 5 (L1)
  5. For L1, check Account is automatically unlocked after and set 30 minutes. For L2/L3, leave automatic unlock unchecked so accounts stay locked until an administrator unlocks them
  6. Click Update Policy

Code Implementation

Code Pack: API Script
hth-okta-1.05-configure-account-lockout.sh View source on GitHub ↗
CURRENT=$(okta_get "/api/v1/policies/${POLICY_ID}")
UPDATED=$(printf '%s' "${CURRENT}" | jq \
  --argjson max "${LOCKOUT_THRESHOLD}" --argjson unlock "${AUTO_UNLOCK_MINUTES}" '
  .settings.password.lockout.maxAttempts = $max
  | .settings.password.lockout.autoUnlockMinutes = $unlock
  | .settings.password.lockout.showLockoutFailures = true')
if okta_put "/api/v1/policies/${POLICY_ID}" "${UPDATED}" > /dev/null; then
  updated=$((updated + 1))
else
  fail "1.5 Failed to update lockout for policy '${POLICY_NAME}'"
  failed=$((failed + 1))
fi

Validation

  1. Navigate to: Security → Authenticators → Password → Edit
  2. For each policy, verify lockout settings are configured

Note: If Okta relies on external directory services for user sourcing, account lockout is managed by the connected directory service.


1.6 Configure Account Lifecycle Management

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 AC-2(3)
DISA STIG V-273188

Description

Automatically disable user accounts after a period of inactivity to reduce the risk of dormant account compromise. Attackers targeting inactive accounts may maintain undetected access since account owners won’t notice unauthorized activity.

Rationale

Why This Matters:

  • Dormant accounts retain valid credentials and application entitlements while no one is watching them, making them ideal takeover targets
  • A legitimate owner who has stopped using an account will not notice unauthorized sign-ons, password resets, or new factor enrollments
  • Inactive accounts often belong to departed staff, contractors, or abandoned service identities that should no longer have access at all
  • Auto-suspending stale accounts shrinks the standing attack surface without waiting on manual deprovisioning

Attack Prevented: Dormant account takeover, orphaned-account abuse, undetected persistence

Specification Requirements

Requirement L1 (Crawl) L2/L3 (DISA STIG)
Inactivity threshold 90 days 35 days
Action Suspend Suspend

Prerequisites

  • Okta Workflows license (required for Automations)
  • Super Admin or Org Admin access

ClickOps Implementation

Step 1: Create Inactivity Automation

  1. Navigate to: Workflow → Automations
  2. Click Add Automation
  3. Enter a name (e.g., “User Inactivity - Auto Suspension”)

Step 2: Configure Trigger Condition

  1. Click Add Condition
  2. Select User Inactivity in Okta
  3. Set duration to 35 days (L2/L3) or 90 days (L1)
  4. Click Save

Step 3: Configure Schedule

  1. Click the edit button next to Select Schedule
  2. Set Schedule field to Run daily
  3. Set Time field to an appropriate time (e.g., 2:00 AM local time)
  4. Click Save

Step 4: Configure Scope

  1. Click the edit button next to Select group membership
  2. In the Applies to field, select Everyone
  3. Click Save

Step 5: Configure Action

  1. Click Add Action
  2. Select Change user lifecycle state in Okta
  3. In Change user state to, select Suspended
  4. Click Save

Step 6: Activate Automation

  1. Click the Inactive button near the top of the screen
  2. Select Activate

Automation: ClickOps only — Okta exposes no public write interface for Automations. The console stores them as USER_LIFECYCLE policies, a type absent from the Okta Management API’s documented policy types (ACCESS_POLICY, ENTITY_RISK, IDP_DISCOVERY, MFA_ENROLL, OKTA_SIGN_ON, PASSWORD, POST_AUTH_SESSION, PROFILE_ENROLLMENT, and others), and no documented endpoint creates one (Okta Policy API, management API spec 2026.08.4, checked 2026-09-24).

Validation

  1. Navigate to: Workflow → Automations
  2. Verify the automation is listed and shows Active status
  3. Review the automation history after the first scheduled run

Note: If Okta relies on external directory services (e.g., Active Directory) for user sourcing, this automation may not be applicable. The connected directory service must perform this function instead.


1.7 Configure PIV/CAC Smart Card Authentication

Profile Level: L3 (Run)

Framework Control
NIST 800-53 IA-2(12)
DISA STIG V-273204, V-273207

Description

Configure Okta to accept Personal Identity Verification (PIV) credentials and Common Access Cards (CAC) for authentication. This enables hardware-based multifactor authentication using approved certificate authorities.

Rationale

Why This Matters:

  • PIV/CAC smart cards bind authentication to a hardware-held private key validated against an approved certificate authority, so the credential cannot be phished or replayed
  • Certificate-based authentication satisfies the AAL3 hardware-bound assurance required for high-sensitivity and U.S. Government systems
  • Unlike passwords or shared secrets, the smart card credential never traverses the network in a reusable form
  • Matching the certificate identifier (e.g., EDIPI) to the Okta profile ties every login to a vetted, government-issued identity

Attack Prevented: Credential phishing, password theft, replay attacks, unauthorized non-PIV access

Prerequisites

  • Super Admin access
  • Approved certificate chain (root and intermediate CA certificates)
  • Smart Card IdP capability in your Okta edition

ClickOps Implementation

Step 1: Configure Smart Card Identity Provider

  1. Navigate to: Security → Identity Providers
  2. Click Add identity provider
  3. Select Smart Card IdP and click Next
  4. Enter a name for the identity provider (e.g., “CAC Authentication”)
  5. Under Security Characteristics, mark the IdP PIN protected and Hardware protected

Step 2: Build Certificate Chain

  1. Click Browse to select your root CA certificate file
  2. Click Add Another to add intermediate CA certificates
  3. Continue until the complete certificate chain is uploaded
  4. Click Build certificate chain
  5. Verify the chain builds successfully with all certificates shown
  6. If errors occur, verify certificate order and format

Step 3: Configure User Matching

  1. In IdP username, select idpuser.subjectAltNameUpn
    • This attribute stores identifiers like the Electronic Data Interchange Personnel Identifier (EDIPI)
  2. In Match against, select the Okta Profile Attribute where the identifier is stored
  3. Click Finish

Step 4: Activate the Identity Provider

  1. Verify the IdP status shows Active
  2. If inactive, click Activate

Step 5: Add the Smart Card Authenticator

  1. Navigate to: Security → Authenticators
  2. In the Setup tab, click Add authenticator
  3. Select Smart Card Authenticator (“Authentication with a Security Card (PIV/CAC)”)
  4. Select the Smart Card IdP created in Step 1 and click Add

Code Implementation

Code Pack: API Script
hth-okta-1.07-configure-piv-cac-smart-card-authentication.sh View source on GitHub ↗
# Smart Card identity providers (a failed read stops the pack)
SMART_CARD_IDPS=$(okta_get "/api/v1/idps?type=X509" | jq -c '[.[] | select(.type == "X509")]')
IDP_COUNT=$(printf '%s' "${SMART_CARD_IDPS}" | jq 'length')
ACTIVE_IDPS=$(printf '%s' "${SMART_CARD_IDPS}" | jq '[.[] | select(.status == "ACTIVE")] | length')
printf '%s' "${SMART_CARD_IDPS}" | jq -r '.[] | "  - \(.name) (status: \(.status), created: \(.created))"'

# Smart Card authenticator, which points at a Smart Card IdP
SMART_CARD_AUTH=$(okta_get "/api/v1/authenticators" \
  | jq -r '[.[] | select(.key == "smart_card_idp")][0].status // "ABSENT"')

Validation

  1. Navigate to: Security → Identity Providers
  2. Verify Smart Card IdP is listed with Type as “Smart Card”
  3. Verify Status is “Active”
  4. Click Actions → Configure and verify certificate chain is from approved CA

1.8 Configure FIPS-Compliant Authenticators

Profile Level: L3 (Run)

Framework Control
NIST 800-53 SC-13
DISA STIG V-273205

Description

Configure Okta Verify to only connect with FIPS-compliant devices. This ensures that authentication uses FIPS 140-2 validated cryptographic modules.

Rationale

Why This Matters:

  • Non-validated cryptographic implementations may contain weaknesses that allow key extraction or signature forgery against the authenticator
  • FIPS 140-2 validation provides assurance that the cryptographic module has been independently tested against a government standard
  • Regulated and federal environments mandate FIPS-validated cryptography for any authentication touching protected systems
  • Restricting Okta Verify to FIPS-compliant devices prevents enrollment of authenticators that fail to meet the required cryptographic baseline

Attack Prevented: Cryptographic weakness exploitation, non-compliant authenticator enrollment, key compromise

Prerequisites

  • Super Admin access
  • Okta Verify authenticator enabled
  • Users with FIPS-compliant devices (devices that support FIPS 140-2 mode)

ClickOps Implementation

Step 1: Edit Okta Verify Settings

  1. Navigate to: Security → Authenticators
  2. In the Setup tab, click Actions next to Okta Verify → Edit

Step 2: Enable FIPS Compliance

  1. Locate the FIPS Compliance field (Okta: Configure Okta Verify options)
  2. Select FIPS-compliant devices only
  3. Click Save

Note: Not every org’s Okta Verify form shows this field — it was absent on an Okta Integrator Free Plan org checked 2026-09-24, although that org’s Authenticators API still returned the underlying setting (settings.compliance.fips); see Code Implementation.

Step 3: Require FIPS-Compliant Passkeys

  1. In Security → Authenticators, click Actions next to Passkey (FIDO2 WebAuthn) → Edit, then Edit on its settings page
  2. Under Required characteristics, check FIPS compliant
  3. Click Save

Code Implementation

Code Pack: API Script
hth-okta-1.08-configure-fips-compliant-authenticators.sh View source on GitHub ↗
OKTA_VERIFY=$(okta_get "/api/v1/authenticators" | jq -c '[.[] | select(.key == "okta_verify")][0] // empty')
if [ -z "${OKTA_VERIFY}" ]; then
  fail "1.8 Okta Verify authenticator not found"
  increment_failed
  summary
fi
OV_ID=$(printf '%s' "${OKTA_VERIFY}" | jq -r '.id')
CURRENT_FIPS=$(printf '%s' "${OKTA_VERIFY}" | jq -r '.settings.compliance.fips // "unset"')
info "1.8 Okta Verify FIPS compliance is currently: ${CURRENT_FIPS}"

if [ "${CURRENT_FIPS}" = "REQUIRED" ]; then
  pass "1.8 Okta Verify already requires FIPS-compliant devices"
  increment_applied
  summary
  exit 0
fi

UPDATED=$(printf '%s' "${OKTA_VERIFY}" | jq '.settings.compliance.fips = "REQUIRED"')
if okta_put "/api/v1/authenticators/${OV_ID}" "${UPDATED}" > /dev/null; then
  pass "1.8 Okta Verify now requires FIPS-compliant devices"
  increment_applied
else
  fail "1.8 Failed to update Okta Verify FIPS compliance"
  increment_failed
fi

Validation

  1. Navigate to: Security → Authenticators
  2. From the Setup tab, click Actions next to Okta Verify → Edit
  3. Verify FIPS Compliance is set to “FIPS-compliant devices only”

Note: Enabling FIPS-compliant devices only will prevent users with non-FIPS compliant devices from enrolling in Okta Verify. Ensure users have compatible devices before enabling this setting.


1.9 Audit Default Authentication Policy

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 AC-3, IA-2

Description

Audit and mitigate the risk posed by Okta’s default app sign-in policy — the system policy every new application is assigned to unless it is explicitly moved. The default policy cannot be deleted, and in older orgs its catch-all rule permits password-only sign-in. Any application that falls through to a weak default policy bypasses the MFA enforcement configured elsewhere.

Rationale

Why This Matters:

  • Every org has one default app sign-in policy (“Default Policy” in older orgs, “Any two factors” in newer ones); in older orgs its catch-all rule allows single-factor (password-only) authentication
  • The default policy cannot be deleted, and its requirements are only as strong as its Catch-all Rule — which admins often never open
  • It serves as the final catch-all: any application not assigned to a stronger policy silently uses the default
  • New applications added to the tenant are assigned to the default policy unless explicitly moved
  • Organizations often believe MFA is enforced globally, unaware that the default backstop allows password-only access

Attack Prevented: MFA bypass via policy gap exploitation. An attacker who discovers an application assigned to the default policy can authenticate with stolen credentials alone, completely circumventing phishing-resistant MFA controls configured in other policies.

Real-World Context:

  • Obsidian Security Research: Identified that a significant percentage of Okta tenants have applications inadvertently assigned to the default policy, creating silent MFA gaps in otherwise hardened environments

ClickOps Implementation

Step 1: Identify the Default App Sign-In Policy

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. Open the policy marked Default (“Any two factors” in current orgs, “Default Policy” in older ones)
  3. Inspect its Catch-all Rule (Actions → Edit) — the rule is editable, so confirm it requires MFA rather than a password alone

Step 2: Audit Application Policy Assignments

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. For each policy, click the Application tab
  3. Document which applications are assigned to each policy
  4. Critical: Check the default policy’s Application tab
  5. Every application listed there gets only the default policy’s requirements — move it to an explicit policy

Step 3: Reassign Applications to Explicit Policies

  1. For each application assigned to the default policy:
    • Navigate to: Applications and Resources → Applications → [App Name]
    • Click the Sign On tab
    • Under User authentication, click Edit
    • In Authentication policy, select a custom policy that enforces MFA
    • Click Save
  2. Repeat until the default policy has zero applications assigned

Step 4: Create a Catch-All Deny Rule in Custom Policies

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. For each custom policy:
    • Click Add rule
    • Name: “Catch-All Deny”
    • IF: Any user, any device, any network
    • THEN Access is: Denied
    • Position this rule as the second-to-last rule (above only the Catch-all Rule)
  3. This ensures that any request not explicitly permitted by a higher-priority rule is denied rather than falling through

Step 5: Establish Ongoing Governance

  1. Create a recurring calendar reminder (monthly) to re-audit policy assignments
  2. Document the policy assignment standard in your security runbook
  3. Include policy assignment verification in your application onboarding checklist

Time to Complete: ~45 minutes (initial audit) + 5 minutes per application reassignment

Code Implementation

Code Pack: Terraform
hth-okta-1.09-audit-default-auth-policy.tf View source on GitHub ↗
# Reference the immutable Default Authentication Policy
data "okta_policy" "default_access" {
  name = "Default Policy"
  type = "ACCESS_POLICY"
}

# Custom catch-all policy to replace reliance on the default policy
resource "okta_app_signon_policy" "mfa_required" {
  name        = "MFA Required - All Applications"
  description = "Enforces MFA for all applications - prevents fallthrough to default policy"
}

# Catch-all deny rule (lowest priority in custom policy)
resource "okta_app_signon_policy_rule" "catch_all_deny" {
  policy_id          = okta_app_signon_policy.mfa_required.id
  name               = "Catch-All Deny"
  priority           = 99
  access             = "DENY"
  factor_mode        = "2FA"
  constraints        = []
  groups_excluded    = []
  groups_included    = ["EVERYONE"]
  network_connection = "ANYWHERE"
}

# MFA enforcement rule (higher priority than catch-all)
resource "okta_app_signon_policy_rule" "require_mfa" {
  policy_id                   = okta_app_signon_policy.mfa_required.id
  name                        = "Require MFA"
  priority                    = 1
  access                      = "ALLOW"
  factor_mode                 = "2FA"
  groups_included             = ["EVERYONE"]
  network_connection          = "ANYWHERE"
  re_authentication_frequency = "PT2H"
}
Code Pack: API Script
hth-okta-1.09-audit-default-auth-policy.sh View source on GitHub ↗
# Find the default app sign-in policy (system=true), whatever it is named
DEFAULT_POLICY=$(okta_get "/api/v1/policies?type=ACCESS_POLICY" \
  | jq -c '[.[] | select(.system == true)][0] // empty')
# List apps assigned to the default policy (a failed read stops the pack)
DEFAULT_APPS=$(okta_get "/api/v1/policies/${DEFAULT_POLICY_ID}/app")
APP_COUNT=$(printf '%s' "${DEFAULT_APPS}" | jq 'length')
Code Pack: Sigma Detection Rule
hth-okta-1.09-audit-default-auth-policy.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'policy.evaluate_sign_on'
        debugContext.debugData.policyType: 'ACCESS_POLICY'
    filter_default_policy:
        target.displayName|contains: 'Default Policy'
    condition: selection and filter_default_policy
fields:
    - actor.displayName
    - client.ipAddress
    - outcome.result
    - published

Validation & Testing

  1. Run API query to list all apps assigned to the Default Policy – result should be zero applications
  2. Attempt login to a test application with password only – should be denied by catch-all rule
  3. Attempt login to a test application with password + MFA – should succeed
  4. Add a new test application and verify it is not automatically assigned to the Default Policy
  5. Review System Log for policy.evaluate_sign_on events referencing the Default Policy – should show no recent hits
  6. Verify each custom policy has a catch-all deny rule as the second-to-last rule

Expected result: Zero applications assigned to the Default Policy; all authentication flows require MFA through explicit custom policies.

Monitoring & Maintenance

Detection rule: See the Sigma rule in Code Pack section 1.9 above.

Maintenance schedule:

  • Weekly: Automated script to check for apps on Default Policy (integrate into CI/CD)
  • Monthly: Manual review of authentication policy assignments
  • On application onboarding: Mandatory policy assignment as part of app deployment checklist
  • Quarterly: Full audit of all policy rules and catch-all deny rule placement

1.10 Harden Self-Service Recovery

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 IA-5(1), IA-11

Description

Restrict self-service account recovery to trusted methods and network locations. Remove weak recovery options (SMS, voice call, security questions) that are susceptible to interception, SIM swapping, and social engineering. Limit recovery flows to corporate network zones to prevent account hijacking from untrusted locations.

Rationale

Why This Matters:

  • Self-service password recovery is a primary account takeover vector – attackers bypass MFA by resetting credentials
  • SMS-based recovery is vulnerable to SIM swapping, SS7 interception, and carrier social engineering
  • Voice call recovery is susceptible to call forwarding attacks and voicemail compromise
  • Security questions can be researched or socially engineered (mother’s maiden name, first pet, etc.)
  • Recovery flows initiated from untrusted networks allow attackers to reset passwords remotely without triggering network-based controls
  • Once an attacker resets a password, they can enroll their own MFA factors and establish persistent access

Attack Prevented: Account takeover via password recovery abuse. An attacker with access to a target’s phone number (via SIM swap) or personal information (via OSINT) initiates self-service recovery, resets the password, enrolls a new authenticator, and gains persistent access to all SSO-connected applications.

Real-World Context:

  • Obsidian Security Research: Identified that recovery flows from untrusted networks are a top account hijack technique, especially when SMS or security questions are enabled as recovery options

Prerequisites

  • Super Admin access
  • Network zones configured (see Section 2.1)
  • Corporate network zone defined with VPN egress IPs
  • Okta Verify or email-based authenticator deployed to users

ClickOps Implementation

Step 1: Remove Weak Recovery Authenticators

  1. Navigate to: Security → Authenticators
  2. Review the authenticators listed on the Setup tab
  3. For Phone (SMS / Voice call), if it is listed:
    • Click Actions → Edit
    • Set its use to authentication only, so it can no longer serve recovery
    • If SMS/Voice is not needed at all, click Actions → Deactivate
  4. For Security Question, if it is listed:
    • Click Actions → Deactivate
    • Confirm deactivation
    • Note: Existing enrolled security questions will be removed from user accounts

Step 2: Configure Password Recovery Settings Recovery authenticators are set in each password policy’s rule, not in the policy itself:

  1. Navigate to: Security → Authenticators
  2. Click Actions next to Password → Select Edit
  3. For each password policy, click Edit rule on its rule:
    • Users can initiate recovery with: keep only Okta Verify (Push notification only) and Email; clear Phone (SMS / Voice call) and Google Authenticator
    • Additional verification is: Any enrolled authenticator used for MFA/SSO
    • Access control: Authentication policy (so the Okta account management policy in Step 3 governs recovery)
  4. Click Update rule for each policy

Step 3: Restrict Recovery to Corporate Network Zones Self-service recovery is governed by the Okta account management policy (or, for rules left on the legacy setting, by the password policy rule itself):

  1. Navigate to: Security → Authentication Policies → Okta account management
  2. Open the Okta Account Management Policy and click Add rule:
    • Name: “Block Recovery from Untrusted Networks”
    • IF User’s IP is: Not in any of the following zones → “Corporate Network” (or your defined trusted zone)
    • THEN Access is: Denied (or require a phishing-resistant factor)
  3. Position this rule above your general allow rules
  4. Click Save
  5. For any password policy rule still on This rule (legacy) access control, set User’s IP is: In zone “Corporate Network” on that rule instead

Step 4: Configure Authenticator Enrollment Policy

  1. Navigate to: Security → Authenticators → Enrollment tab
  2. Edit the enrollment policy:
    • Okta Verify: Set to Required
    • Email: Set to Required
    • Phone: Set to Disabled or Optional (not for recovery)
    • Security Question: Set to Disabled
  3. Click Save

Step 5: Test Recovery Flow

  1. From a corporate network IP, initiate a test password recovery
  2. Verify only email and authenticator-based options are presented
  3. From an external/untrusted IP, attempt recovery – verify it is blocked or requires step-up

Time to Complete: ~30 minutes

Code Implementation

Code Pack: Terraform
hth-okta-1.10-harden-self-service-recovery.tf View source on GitHub ↗
# Deactivate security question authenticator
resource "okta_authenticator" "security_question" {
  name   = "Security Question"
  key    = "security_question"
  status = "INACTIVE"
}

# Configure phone authenticator -- remove recovery usage, keep for sign-in only.
# Okta's allowedFor values are "any", "none", "recovery", and "sso" (sign-in).
resource "okta_authenticator" "phone" {
  name   = "Phone"
  key    = "phone_number"
  status = "ACTIVE"
  settings = jsonencode({
    allowedFor = "sso"
  })
}

# Password policy with hardened recovery settings
resource "okta_policy_password" "hardened_recovery" {
  name                     = "Hardened Password Policy"
  status                   = "ACTIVE"
  description              = "Password policy with restricted recovery methods"
  priority                 = 1
  password_min_length      = var.password_min_length
  password_min_lowercase   = 1
  password_min_uppercase   = 1
  password_min_number      = 1
  password_min_symbol      = 1
  password_max_age_days    = var.password_max_age_days
  password_min_age_minutes = 1440
  password_history_count   = var.password_history_count
  recovery_email_token     = 1
  email_recovery           = "ACTIVE"
  sms_recovery             = "INACTIVE"
  call_recovery            = "INACTIVE"
  question_recovery        = "INACTIVE"

  groups_included = [var.everyone_group_id]
}
Code Pack: API Script
hth-okta-1.10-harden-self-service-recovery.sh View source on GitHub ↗
# Recovery may start only with Okta Verify push or email -- never SMS, voice, or OTP
RULE=$(okta_get "/api/v1/policies/${POLICY_ID}/rules/${RULE_ID}")
UPDATED=$(printf '%s' "${RULE}" | jq '
  .actions.selfServicePasswordReset.requirement.primary.methods = ["push", "email"]
  | del(.actions.selfServicePasswordReset.requirement.primary.methodConstraints)')
if okta_put "/api/v1/policies/${POLICY_ID}/rules/${RULE_ID}" "${UPDATED}" > /dev/null; then
  updated=$((updated + 1))
else
  fail "1.10 Failed to update rule ${RULE_ID} in policy '${POLICY_NAME}'"
  failed=$((failed + 1))
fi
# Step 2: Deactivate the Security Question authenticator if it is active
SECURITY_QUESTION=$(printf '%s' "${AUTHENTICATORS}" | jq -c '[.[] | select(.key == "security_question")][0] // empty')
if [ -n "${SECURITY_QUESTION}" ] && [ "$(printf '%s' "${SECURITY_QUESTION}" | jq -r '.status')" = "ACTIVE" ]; then
  SQ_ID=$(printf '%s' "${SECURITY_QUESTION}" | jq -r '.id')
  if okta_post "/api/v1/authenticators/${SQ_ID}/lifecycle/deactivate" '{}' > /dev/null; then
    info "1.10 Security Question authenticator deactivated"
  else
    fail "1.10 Failed to deactivate the Security Question authenticator"
    failed=$((failed + 1))
  fi
else
  info "1.10 Security Question authenticator is not active"
fi
# Step 3: Phone may still serve sign-in (allowedFor "sso") but never recovery
PHONE=$(printf '%s' "${AUTHENTICATORS}" | jq -c '[.[] | select(.key == "phone_number")][0] // empty')
if [ -n "${PHONE}" ]; then
  PHONE_ID=$(printf '%s' "${PHONE}" | jq -r '.id')
  PHONE_UPDATED=$(printf '%s' "${PHONE}" | jq '.settings.allowedFor = "sso"')
  if okta_put "/api/v1/authenticators/${PHONE_ID}" "${PHONE_UPDATED}" > /dev/null; then
    info "1.10 Phone authenticator restricted to sign-in only (allowedFor: sso)"
  else
    fail "1.10 Failed to update the Phone authenticator"
    failed=$((failed + 1))
  fi
fi
Code Pack: Sigma Detection Rules (2)
hth-okta-1.10-harden-self-service-recovery-b.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'user.account.reset_password'
        debugContext.debugData.factor:
            - 'SMS'
            - 'CALL'
            - 'QUESTION'
    condition: selection
fields:
    - actor.displayName
    - client.ipAddress
    - outcome.result
    - published

hth-okta-1.10-harden-self-service-recovery.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'user.account.reset_password'
            - 'user.credential.forgot_password'
        securityContext.isProxy: true
    condition: selection
fields:
    - actor.displayName
    - client.ipAddress
    - client.geographicalContext.city
    - client.geographicalContext.country
    - outcome.result
    - published

Validation & Testing

  1. Navigate to Security → Authenticators and verify Security Question shows Inactive
  2. Navigate to Security → Authenticators → Password → Edit and verify SMS, Voice, and Security Question are disabled for recovery
  3. Initiate a password reset from a corporate network IP – verify only Email and Okta Verify options appear
  4. Initiate a password reset from an external/untrusted IP – verify the request is blocked or requires additional verification
  5. Attempt to enroll a security question as a user – should not be available
  6. Review System Log for user.account.reset_password events and verify they originate only from trusted network zones
  7. Verify recovery token lifetime is set to 10 minutes or less

Expected result: Self-service recovery limited to email and authenticator-based methods; no SMS, voice, or security question options; recovery blocked from untrusted networks.

Monitoring & Maintenance

Detection rules: See the Sigma rules in Code Pack section 1.10 above.

Maintenance schedule:

  • Monthly: Verify authenticator enrollment policy still disables weak recovery options
  • Quarterly: Audit recovery events in System Log for anomalies
  • On policy changes: Re-verify that recovery restrictions remain in place after any authenticator or policy modifications
  • Annually: Review recovery methods against current threat landscape (new attack techniques against remaining methods)

1.11 Enable End-User Security Notifications

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 SI-4, IR-6

Description

Enable all four end-user security notification emails in Okta so that users receive immediate alerts when security-relevant changes occur on their accounts. Additionally enable Suspicious Activity Reporting to allow users to flag unauthorized actions directly from notification emails, creating actionable system log events for security teams.

Rationale

Why This Matters:

  • End users are often the first to notice unauthorized access to their accounts – a notification about an unrecognized sign-on or authenticator change prompts immediate reporting
  • Without notifications, an attacker who compromises an account can operate undetected for days or weeks while enrolling new factors, changing passwords, and accessing applications
  • Authenticator enrolled/reset notifications detect a critical persistence technique: attackers who gain temporary access immediately register their own MFA factors to maintain access after the initial vector is closed
  • Suspicious Activity Reporting turns every user into a sensor – when a user clicks “Report Suspicious Activity” in a notification email, Okta generates a user.account.report_suspicious_activity_by_enduser system log event that SIEM can automatically escalate
  • These notifications cost nothing to enable and provide significant detection value with no user friction

Attack Prevented: Undetected account takeover and persistence. An attacker who compromises credentials and enrolls a new authenticator will trigger an “authenticator enrolled” notification to the legitimate user, who can immediately report the unauthorized change before the attacker establishes persistent access.

Real-World Context:

  • Okta HealthInsight: Flags missing end-user notifications as a security gap in tenant health assessments
  • Obsidian Security Research: Recommends enabling the end-user notification emails as a low-effort, high-value detection control

ClickOps Implementation

Step 1: Enable the Security Notification Emails

  1. Navigate to: Security → General
  2. In the Security notification emails section, click Edit
  3. Enable all four notification emails:
Notification Description Enable
New sign-on notification email Alerts users when a sign-on occurs from an unrecognized device or browser Yes
Password changed notification email Alerts users when their password is changed Yes
Authenticator enrolled notification email Alerts users when a new authenticator (MFA factor) is registered to their account Yes
Authenticator reset notification email Alerts users when an authenticator is removed or reset on their account, including by an administrator Yes
  1. Click Save

Step 2: Enable Suspicious Activity Reporting

  1. In the same Security notification emails section, enable Report suspicious activity via email
  2. Click Save
  3. When enabled, notification emails include a “Report Suspicious Activity” button
  4. User clicks generate a system log event: user.account.report_suspicious_activity_by_enduser

Step 3: Verify Notification Delivery

  1. Using a test user account, perform a sign-on from a new browser or device
  2. Verify the test user receives a “New sign-on” notification email
  3. Verify the email contains the “Report Suspicious Activity” button (if Suspicious Activity Reporting is enabled)
  4. Click “Report Suspicious Activity” and verify the system log event is created

Step 4: Configure SIEM Alerting for Suspicious Activity Reports

  1. In your SIEM, create a high-priority alert for the event type user.account.report_suspicious_activity_by_enduser
  2. This event should trigger an immediate incident response workflow
  3. Correlate with recent authentication and factor enrollment events for the reporting user

Time to Complete: ~15 minutes

Code Implementation

Code Pack: Terraform
hth-okta-1.11-enable-security-notifications.tf View source on GitHub ↗
resource "okta_security_notification_emails" "end_user_notifications" {
  send_email_for_new_device_enabled        = true
  send_email_for_password_changed_enabled  = true
  send_email_for_factor_enrollment_enabled = true
  send_email_for_factor_reset_enabled      = true
  report_suspicious_activity_enabled       = true
}
Code Pack: Sigma Detection Rules (2)
hth-okta-1.11-enable-security-notifications-b.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'user.mfa.factor.activate'
            - 'user.mfa.factor.enroll'
            - 'system.mfa.factor.activate'
            - 'user.account.update_password'
    condition: selection
fields:
    - actor.displayName
    - target.displayName
    - client.ipAddress
    - client.userAgent.rawUserAgent
    - published

hth-okta-1.11-enable-security-notifications.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'user.account.report_suspicious_activity_by_enduser'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - client.ipAddress
    - client.geographicalContext.city
    - client.geographicalContext.country
    - outcome.result
    - published

The Okta Management API documents no endpoint for these settings; the Terraform resource above uses an internal Okta endpoint (as its provider documentation states) and needs an SSWS API token.

Validation & Testing

  1. Navigate to Security → General → Security notification emails and verify all four notification emails are Enabled
  2. In the same section, verify Report suspicious activity via email is Enabled
  3. Sign in with a test user from a new device/browser – verify “New sign-on” email is received
  4. Enroll a new authenticator for a test user – verify “Authenticator enrolled” email is received
  5. Reset an authenticator for a test user – verify “Authenticator reset” email is received
  6. Change a test user’s password – verify “Password changed” email is received
  7. In a notification email, click “Report Suspicious Activity” – verify the system log event user.account.report_suspicious_activity_by_enduser is created
  8. Verify SIEM alert fires for the suspicious activity report event

Expected result: All four notification emails active; users receive timely emails for security-relevant account changes; suspicious activity reports generate system log events that trigger SIEM alerts.

Monitoring & Maintenance

Detection rules: See the Sigma rules in Code Pack section 1.11 above.

Incident response workflow for suspicious activity reports:

  1. SIEM receives user.account.report_suspicious_activity_by_enduser event
  2. Automatically create incident ticket with HIGH priority
  3. Pull last 24 hours of authentication and factor events for the reporting user
  4. Check for: new factor enrollments, password changes, sign-ons from unusual locations
  5. If compromise indicators found: suspend user session, force re-authentication, reset factors

Maintenance schedule:

  • Monthly: Review suspicious activity report volume and response times
  • Quarterly: Verify all four notification emails are still enabled (configuration drift check)
  • Quarterly: Test notification delivery by performing a controlled sign-on from a new device
  • Annually: Review notification types against Okta feature updates (new notification types may be added)

1.12 Enforce Device Assurance Policies

Profile Level: L2 (Walk)

Framework Control
CIS Controls 4.1, 7.3
NIST 800-53 CM-6, IA-3, SI-2

Description

Device Assurance policies define the minimum security posture a device must meet before its user is permitted to authenticate, and they are bound to access decisions through device conditions in application sign-in policy rules. Each policy is scoped to a single platform (Android, ChromeOS, iOS, macOS, or Windows) and evaluates signals such as minimum OS version and patch level, disk encryption, screen lock, jailbreak or root status, and secure hardware presence, collected by Okta Verify or by Chrome Device Trust on managed Chrome browsers. See Okta’s Device assurance policies guide and the Device Assurance Policies API (diskEncryptionType, screenLockType, secureHardwarePresent, jailbreak, osVersion) for the full attribute model.

Rationale

Why This Matters:

  • Phishing-resistant MFA proves who is signing in but says nothing about what they are signing in from — a correctly authenticated user on a compromised endpoint hands a valid session to whatever else is running on that device
  • Minimum OS version and patch-level requirements deny authentication from devices carrying known-exploited vulnerabilities instead of waiting for endpoint management to catch up
  • Jailbreak and root detection prevents attackers from defeating the platform keystore protections that Okta Verify and FastPass rely on for device-bound credentials
  • Device Assurance is the enforcement point for blocking the outdated Okta Verify and Okta Browser Plugin versions identified through the advisory monitoring process in Section 7.2
  • Okta Verify Advanced Posture Checks (early access, announced February 2026) extends the model with administrator-written osquery rules that evaluate device hygiene at sign-in — installed applications, persistent services, running processes, binaries and configuration files in common install paths, Homebrew and npm packages, listening ports, and Docker artifacts — so unwanted software on a corporate device denies access at the authentication decision rather than surfacing hours later in an EDR console. See Okta Threat Intelligence, Detecting OpenClaw

Attack Prevented: Authentication from compromised, unpatched, jailbroken, or unmanaged endpoints; session theft from devices running credential-stealing or unsanctioned remote-access software; policy bypass via outdated client versions

Prerequisites

  • Okta Identity Engine tenant with Device Assurance available in your edition
  • Okta Verify deployed to managed endpoints, or Chrome Device Trust configured for managed browsers
  • Device registration or endpoint management integration already in place (see Section 1.3)
  • Super Admin access

ClickOps Implementation

Step 1: Confirm the Device Signal Source

  1. Navigate to: Security → Authenticators and confirm Okta Verify is active with Okta FastPass enabled (see Section 1.3)
  2. For managed Chrome browsers, plan to select Chrome Device Trust under Device attribute provider(s) in each device assurance policy (Step 2)
  3. Navigate to: Directory → Devices and confirm managed endpoints are reporting (filter Device management: Managed)

Step 2: Create a Device Assurance Policy per Platform

  1. Navigate to: Security → Device Assurance Policies
  2. Click Add a policy
  3. Enter a descriptive name that includes the platform (e.g., “Windows — Corporate Baseline”)
  4. Select the Platform: Android, ChromeOS, iOS, macOS, or Windows — one policy per platform
  5. Configure the attributes available for that platform:
Attribute Recommended Setting
OS version Current vendor-supported release at the latest security patch level
Disk encryption Required
Lock screen Required
Jailbroken or rooted device Blocked (iOS and Android)
Secure Enclave (macOS) / Hardware keystore (Android) / Trusted Platform Module (Windows) Required
  1. Click Save
  2. Repeat for every platform present in your fleet — a platform with no policy is a platform with no assurance

Step 3: Bind the Policy to Application Sign-In Rules

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. Select the policy protecting your most sensitive applications — start with the Okta Admin Console policy
  3. Click Actions next to the rule you want to harden and select Edit
  4. Set Device state is: Registered, and in Device assurance policy is select the device assurance policies created in Step 2
  5. Set Access is: Allowed after successful authentication only when the assurance policy is satisfied, then add a lower-priority rule that denies access when it is not
  6. Click Save
  7. Repeat for each authentication policy protecting sensitive applications

Step 4: Configure Grace Periods and Remediation Messaging

  1. Re-open each device assurance policy
  2. Where your org’s form offers a grace period for the OS version requirement (No / Yes, by a due date / Yes, after a number of days — the field was not displayed on an Okta Integrator Free Plan org checked 2026-09-24), set one so users are warned before enforcement begins — 7 days for L2, none for L3
  3. Under Remediation, select Display remediation instructions so end users see the specific failed check and how to fix it rather than a generic denial
  4. Click Save

Step 5: Add Advanced Posture Checks (Early Access, Optional)

  1. Navigate to: Security → Advanced Posture Checks → Custom checks (if the page is absent, ask Okta to enable the feature for your tenant)
  2. Author custom osquery rules that assert the absence of prohibited software or the presence of required agents on corporate devices
  3. Attach the custom check to the relevant device assurance policy
  4. Roll the check out in a monitoring posture first, review the sign-in outcomes for false positives, then move to enforcement

Time to Complete: ~2 hours for initial policy creation across platforms, plus fleet remediation time

Code Implementation

Code Pack: API Script
hth-okta-1.12-enforce-device-assurance-policies.sh View source on GitHub ↗
# One policy per platform, each with its posture requirements (a failed read stops the pack)
POLICIES=$(okta_get "/api/v1/device-assurances")
printf '%s' "${POLICIES}" | jq -r '.[] | "  - \(.name) [\(.platform)]: osVersion=\(.osVersion.minimum // .osVersion.dynamicVersionRequirement.type // "none"), diskEncryption=\(.diskEncryptionType.include // [] | join("+") | if . == "" then "none" else . end), screenLock=\(.screenLockType.include // [] | join("+") | if . == "" then "none" else . end), secureHardware=\(if has("secureHardwarePresent") then .secureHardwarePresent else "n/a" end),jailbreak=\(if has("jailbreak") then .jailbreak else "n/a" end)"'

Validation & Testing

  1. Navigate to Security → Device Assurance Policies and verify one active policy exists for every platform in your fleet
  2. Sign in from a compliant managed device — access should succeed
  3. Roll a test device back to an OS version below the minimum and attempt sign-in — access should be denied with remediation guidance displayed
  4. Attempt sign-in from an unregistered or unmanaged device — should be denied by the sign-in policy rule
  5. Attempt sign-in from a jailbroken or rooted test device (iOS or Android) — should be denied
  6. Review the System Log for policy.evaluate_sign_on events and confirm the device assurance condition appears in the evaluation result

Expected result: Only devices meeting the documented posture baseline can authenticate to protected applications; non-compliant devices are denied with actionable remediation guidance.

Monitoring & Maintenance

Maintenance schedule:

  • Monthly: Review denial volume by failed attribute to catch fleet-wide patch lag before it becomes a helpdesk surge
  • Quarterly: Raise the minimum OS version to track vendor support and patch cadence
  • On advisory publication: Update the minimum Okta Verify and Browser Plugin versions per Section 7.2
  • Annually: Review the attribute set against new platform signals added by Okta

Compliance Mappings

Framework Control Requirement
CIS Controls v8 4.1 Establish and maintain a secure configuration process for enterprise assets
CIS Controls v8 7.3 Perform automated operating system patch management
NIST 800-53 CM-6 Configuration settings enforced on endpoints before access is granted
NIST 800-53 IA-3 Device identification and authentication
NIST 800-53 SI-2 Flaw remediation enforced through minimum OS and patch-level requirements
SOC 2 CC6.6 Logical access restricted to devices meeting defined security requirements

1.13 Require Visual Identity Verification for Help Desk Account Actions

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4, 14.2
NIST 800-53 IA-12, IA-5(1), AT-2

Description

Require help desk agents to visually verify a caller’s identity — a live video call with a government-issued or corporate photo ID, or an equivalent out-of-band proofing step — before performing any account recovery action, including password resets, MFA factor resets, and enrollment of a new authenticator. Okta’s own cross-tenant impersonation guidance names this control directly: “Strengthen help desk identity verification processes using visual verification.” (Okta Security — Cross-Tenant Impersonation: Prevention and Detection)

Rationale

Why This Matters:

  • In the August 2023 cross-tenant impersonation campaign, threat actors social-engineered IT service desk personnel into resetting all MFA factors on highly privileged Super Administrator accounts — the technical controls held, the human process did not
  • Every phishing-resistant authenticator configured in Section 1.1 is nullified the moment a help desk agent enrolls an attacker-controlled factor on a legitimate account
  • Voice-only verification depends on knowledge (employee ID, manager name, last four digits of an identifier) that is trivially harvested from OSINT, prior breach corpuses, or an earlier phishing round, and is now cheaply defeated by real-time voice cloning
  • Visual verification against a photo ID on a live video call forces an attacker to defeat a liveness check rather than recite facts, raising the cost of the attack far above the effort of a phone call
  • Factor reset is the highest-privilege operation a help desk agent can perform and it leaves the same log trail as a legitimate reset, so prevention at the process layer is the only reliable control — detection arrives after the attacker already holds the account

Attack Prevented: Help desk social engineering leading to MFA factor reset and account takeover, privileged account impersonation, attacker-controlled authenticator enrollment on Super Admin accounts

ClickOps Implementation

Step 1: Define and Publish the Verification Standard Document a written standard requiring, for every password reset, MFA factor reset, and new authenticator enrollment performed by an agent:

  1. A live video call on a corporate-approved platform — never audio-only, never chat
  2. Presentation of a government-issued or corporate photo ID matched against the identity on file
  3. An agent-initiated callback to the number of record plus a second approver when video is genuinely unavailable
  4. Mandatory manager or security-team approval for any action on an account holding an admin role
  5. A ticket record capturing the verification method, the verifier, the timestamp, and an evidence reference

Step 2: Constrain What the Help Desk Role Can Do

  1. Navigate to: Security → Administrators → Roles
  2. Open the Help Desk custom role created in Section 1.2
  3. Confirm permissions are limited to password reset and account unlock, and exclude okta.users.manage, okta.apps.manage, and all IdP permissions
  4. On the Resources tab, create or edit the resource set assigned with this role and scope it to a group that explicitly excludes all administrators — help desk agents should not be technically capable of acting on privileged accounts
  5. Click Save

Step 3: Require Step-Up Authentication for Privileged Resets

  1. Navigate to: Applications and Resources → Applications → Okta Admin Console → Protected Actions
  2. Click Edit next to Protected actions
  3. Select Reset authenticators for super admins, Reset passwords for super admins, Assign and revoke admin role (excluding super admin role), and Assign and revoke super admin role
  4. Set Authentication required every to 1 minute(s)
  5. Click Save configuration. The step-up factor comes from the Okta Admin Console authentication policy — make it phishing-resistant per Section 1.1

Step 4: Notify the Account Owner on Every Reset

  1. Navigate to: Security → General → Security notification emails and confirm Authenticator reset notification email is Enabled (see Section 1.11)
  2. In the same section, confirm Report suspicious activity via email is Enabled so an account owner can report a reset they did not request

Step 5: Train and Test the Help Desk

  1. Deliver targeted training on the impersonation techniques used against service desks, using the Okta cross-tenant impersonation writeup as the case study
  2. Give agents explicit authority to refuse or escalate any request that skips verification, with no performance penalty for the resulting delay
  3. Run a social engineering exercise at least twice a year, including at least one attempt impersonating an executive or administrator under manufactured time pressure
  4. Track the refusal rate as a control metric alongside ticket resolution time

Time to Complete: ~2 hours for policy drafting, plus training rollout

Automation: ClickOps only — Okta exposes no write interface for this setting: the verification standard and training are procedural, and the Okta Management API documents no Protected Actions endpoint (Okta Management API, spec 2026.08.4, checked 2026-09-24). The help-desk role itself is automatable through Section 1.2’s pack and the resource-set API (/api/v1/iam/resource-sets).

Validation & Testing

  1. A written verification standard exists, is published to the help desk, and names visual verification as mandatory for password resets, factor resets, and new factor enrollment
  2. Review a sample of the last 20 password and factor reset tickets — each must record the verification method and the verifier
  3. Navigate to Security → Administrators → Resources and confirm the help desk resource set excludes all admin accounts
  4. Navigate to Applications and Resources → Applications → Okta Admin Console → Protected Actions and confirm the reset actions require step-up authentication
  5. Conduct an unannounced social engineering test against the help desk — the agent should refuse and escalate
  6. Review System Log user.mfa.factor.reset and user.account.reset_password events and confirm each maps to a ticket documenting visual verification

Expected result: No account recovery action is performed without recorded visual verification, and help desk agents cannot act on privileged accounts at all.

Monitoring & Maintenance

Maintenance schedule:

  • Monthly: Sample recent reset tickets for verification evidence
  • Quarterly: Reconcile System Log reset events against ticket records and investigate any reset without a matching ticket
  • Semi-annually: Run a help desk social engineering exercise and retrain on findings
  • Annually: Review the verification standard against current impersonation techniques, including synthetic voice and video

Compliance Mappings

Framework Control Requirement
CIS Controls v8 5.4 Restrict administrator privileges to dedicated administrator accounts
CIS Controls v8 14.2 Train workforce members to recognize social engineering attacks
NIST 800-53 IA-12 Identity proofing prior to issuing or resetting authenticators
NIST 800-53 IA-5(1) Authenticator management controls for reset and re-issuance
NIST 800-53 AT-2 Role-based security awareness training including social engineering
SOC 2 CC6.1 Logical access credentials issued and reset only to verified individuals

2. Network Access Controls

2.1 Configure IP Zones and Network Policies

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 13.3
NIST 800-53 AC-3, SC-7
DISA STIG V-279691, V-279693 (V1R2)

Description

Define network zones (corporate, VPN, known bad) and enforce authentication policies based on network location. Block or require step-up authentication from untrusted networks.

Rationale

Why This Matters:

  • Attackers often operate from non-corporate infrastructure
  • IP-based policies add defense layer even if credentials stolen
  • Enables geographic restrictions for compliance

Attack Prevented: Credential stuffing from botnets, unauthorized access from foreign locations

ClickOps Implementation

Step 1: Define Network Zones

  1. Navigate to: Security → Networks
  2. Create zones:

Corporate Network:

  • Type: IP Zone
  • IPs: Your office CIDR ranges
  • Gateway IPs: VPN egress IPs

Blocked Locations:

  • Type: Dynamic Zone
  • IP type: Tor anonymizer proxy (or Any proxy); add known-bad Locations or ISP ASNs as needed
  • Either check Block access from IPs matching conditions listed in this zone — a global pre-authentication block that makes the zone unavailable in policies, so skip the deny rule in Step 2 — or leave it unchecked and deny the zone in policy (Step 2)

Step 2: Create Zone-Based Authentication Policy Rules

  1. Navigate to: Security → Authentication Policies → App sign-in → open the policy to harden
  2. Add rule:
    • IF User’s IP is: Not in any of the following zones → “Corporate Network”
    • THEN: require MFA (User must authenticate with: Any 2 factor types) and a shorter re-authentication interval
  3. Add rule (only if “Blocked Locations” is not a blocklist zone):
    • IF User’s IP is: In any of the following zones → “Blocked Locations”
    • THEN Access is: Denied

Code Implementation

Code Pack: Terraform
hth-okta-2.01-configure-network-zones.tf View source on GitHub ↗
# Corporate network zone with configurable CIDRs
resource "okta_network_zone" "corporate" {
  count = length(var.corporate_gateway_cidrs) > 0 ? 1 : 0

  name     = "Corporate Network"
  type     = "IP"
  status   = "ACTIVE"
  gateways = var.corporate_gateway_cidrs
}

# IP blocklist zone
resource "okta_network_zone" "blocklist" {
  count = length(var.blocked_ip_cidrs) > 0 ? 1 : 0

  name     = "Blocked IPs"
  type     = "IP"
  status   = "ACTIVE"
  usage    = "BLOCKLIST"
  gateways = var.blocked_ip_cidrs
}
Code Pack: API Script
hth-okta-2.01-configure-network-zones.sh View source on GitHub ↗
# Create Corporate Network zone
# NOTE: Replace gateway CIDRs with your actual corporate IP ranges
info "2.1 Creating Corporate Network zone..."
if ZONE_RESPONSE=$(okta_post "/api/v1/zones" '{
  "type": "IP",
  "name": "Corporate Network",
  "status": "ACTIVE",
  "usage": "POLICY",
  "gateways": [
    {"type": "CIDR", "value": "203.0.113.0/24"},
    {"type": "CIDR", "value": "198.51.100.0/24"}
  ]
}'); then
  ZONE_ID=$(printf '%s' "${ZONE_RESPONSE}" | jq -r '.id')
  pass "2.1 Corporate Network zone created (ID: ${ZONE_ID})"
  warn "2.1 IMPORTANT: Update the zone with your actual corporate IP ranges"
else
  fail "2.1 Failed to create Corporate Network zone"
  failed=$((failed + 1))
fi
# Dynamic zone matching Tor anonymizer proxies, used as a blocklist
info "2.1 Creating Tor anonymizer block zone..."
if okta_post "/api/v1/zones" '{
  "type": "DYNAMIC",
  "name": "Blocked - Tor Anonymizers",
  "status": "ACTIVE",
  "usage": "BLOCKLIST",
  "proxyType": "Tor"
}' > /dev/null; then
  pass "2.1 Tor anonymizer block zone created"
else
  fail "2.1 Failed to create Tor anonymizer block zone"
  failed=$((failed + 1))
fi

2.2 Restrict Admin Console Access by IP

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 AC-3(7)

Description

Limit access to the Okta Admin Console to specific IP ranges (corporate network, VPN, security team IPs).

Rationale

Why This Matters:

  • The Admin Console grants full control over authentication policies, users, and integrations, making it the single highest-value target in the tenant
  • Restricting console access to known corporate and VPN egress IPs means stolen admin credentials or session tokens cannot be used from arbitrary attacker infrastructure
  • IP allowlisting adds a network-layer control that holds even when credentials or MFA are compromised
  • The October 2023 breach showed that stolen admin sessions are replayed from external networks — an IP allowlist blocks that replay path

Attack Prevented: Stolen admin credential reuse, session token replay from external networks, unauthorized console access

ClickOps Implementation

  1. Navigate to: Security → Networks → Add zone → IP Zone, name it “Admin Allowed IPs”, and enter your corporate egress ranges as Gateway IPs
  2. Navigate to: Security → Authentication Policies → App sign-in → Okta Admin Console
  3. Add rule at priority 1:
    • IF User’s IP is: In any of the following zones → “Admin Allowed IPs”
    • THEN Access is: Allowed after successful authentication
  4. Prove a break-glass administrator path works from an allowed IP first, then edit the Catch-all Rule so Access is: Denied
  5. Optional: Security → General → Organization Security → IP binding for admin console: Enabled binds each admin session to the IP it started on

Warning: A wrong zone on the Okta Admin Console policy locks every administrator out. Ensure a tested break-glass procedure before denying the Catch-all Rule.

Code Implementation

Code Pack: API Script
hth-okta-2.02-restrict-admin-console-access-by-ip.sh View source on GitHub ↗
# The Okta Admin Console is the app named "saasure" (a failed read stops the pack)
ADMIN_APP=$(okta_get "/api/v1/apps?filter=name%20eq%20%22saasure%22" | jq -c '.[0] // empty')
if [ -z "${ADMIN_APP}" ]; then
  fail "2.2 Okta Admin Console app (saasure) not returned -- cannot audit its policy"
  increment_failed
  summary
fi
POLICY_ID=$(printf '%s' "${ADMIN_APP}" | jq -r '._links.accessPolicy.href | split("/") | last')
RULES=$(okta_get "/api/v1/policies/${POLICY_ID}/rules")

ZONED_ALLOW=$(printf '%s' "${RULES}" | jq '[.[] | select(.status == "ACTIVE"
  and .actions.appSignOn.access == "ALLOW"
  and .conditions.network.connection == "ZONE"
  and ((.conditions.network.include // []) | length > 0))] | length')
OPEN_ALLOW=$(printf '%s' "${RULES}" | jq '[.[] | select(.status == "ACTIVE"
  and .actions.appSignOn.access == "ALLOW"
  and ((.conditions.network.connection // "ANYWHERE") == "ANYWHERE"))] | length')

2.3 Configure Dynamic Network Zones and Anonymizer Blocking

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 SC-7, AC-3
DISA STIG V-279692 (V1R2)

Description

Activate Okta’s Enhanced Dynamic Zone to automatically block traffic from anonymizing proxies, Tor exit nodes, and residential proxies. The DefaultEnhancedDynamicZone ships inactive by default and must be explicitly activated.

Rationale

Why This Matters:

  • Attackers use anonymizing proxies, Tor, and VPNs to hide their origin during credential stuffing and session replay attacks
  • Okta’s Enhanced Dynamic Zones leverage IP intelligence to categorize traffic sources automatically
  • The default zone exists but is INACTIVE — many organizations don’t know it’s available
  • Blocking anonymizers reduces attack surface without impacting legitimate users

Attack Prevented: Credential stuffing via anonymized infrastructure, session replay from Tor/proxy networks

ClickOps Implementation

Step 1: Activate the Default Enhanced Dynamic Zone

  1. Navigate to: Security → Networks
  2. Locate DefaultEnhancedDynamicZone in the zone list — it is a system blocklist zone that already includes the ALL_ANONYMIZERS IP service category
  3. Change its status from Inactive to Active. It has no edit form; its only setting is its status

Step 2: Add Finer-Grained Anonymizer Categories (Optional)

  1. Navigate to: Security → Networks → Add zone → Enhanced dynamic zone
  2. Check Block access from IPs matching conditions listed in this zone
  3. Under IP service category, choose Include the following IP service categories and add the anonymizer and proxy categories to block (residential proxies may affect remote workers)
  4. Click Save

Step 3: No Policy Rule Is Needed for Blocklist Zones A zone saved as a blocklist is enforced before any policy is evaluated: the console states that “Configuring a zone as a blocklist makes it unavailable in policies. The configured conditions apply as a pre-authentication deny rule on all Okta endpoints.” Do not try to add these zones to authentication policy rules.

Step 4: Configure Geographic Restrictions (Optional)

  1. Navigate to: Security → Networks
  2. Click Add zone → Dynamic Zone
  3. Configure:
    • Name: “Blocked Countries”
    • Locations: Select countries where your organization has no users
    • Check Block access from IPs matching conditions listed in this zone
  4. Click Save

Code Implementation

Code Pack: Terraform
hth-okta-2.03-block-anonymizers.tf View source on GitHub ↗
# Block anonymizing proxies and Tor exit nodes
resource "okta_network_zone" "block_anonymizers" {
  count = var.profile_level >= 2 ? 1 : 0

  name               = "Block Anonymizers"
  type               = "DYNAMIC_V2"
  status             = "ACTIVE"
  usage              = "BLOCKLIST"
  dynamic_proxy_type = "TorAnonymizer"
}

# Block traffic from high-risk countries
resource "okta_network_zone" "block_countries" {
  count = var.profile_level >= 2 ? 1 : 0

  name              = "Blocked Countries"
  type              = "DYNAMIC"
  status            = "ACTIVE"
  usage             = "BLOCKLIST"
  dynamic_locations = var.blocked_countries
}
Code Pack: API Script
hth-okta-2.03-block-anonymizers.sh View source on GitHub ↗
# Activate the default enhanced dynamic zone (a pre-authentication blocklist)
info "2.3 Activating DefaultEnhancedDynamicZone..."
if okta_post "/api/v1/zones/${ZONE_ID}/lifecycle/activate" '{}' > /dev/null; then
  pass "2.3 Enhanced Dynamic Zone activated with anonymizer blocking"
  increment_applied
else
  fail "2.3 Failed to activate Enhanced Dynamic Zone"
  increment_failed
fi
Code Pack: Sigma Detection Rule
hth-okta-2.03-block-anonymizers.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'security.threat.detected'
        debugContext.debugData.threatSuspected: 'ANONYMIZER'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - client.ipAddress
    - debugContext.debugData.threatSuspected
    - outcome.result
    - published

Validation & Testing

  1. Navigate to Security → Networks and verify DefaultEnhancedDynamicZone shows Active status
  2. Verify the zone is listed as an enhanced dynamic zone blocklist
  3. Test access from a Tor exit node or known anonymizing proxy — should be denied
  4. Verify legitimate users on corporate VPN are not affected

Monitoring & Maintenance

Detection rule: See the Sigma rule in Code Pack section 2.3 above.

Maintenance schedule:

  • Monthly: Review blocked traffic patterns for false positives
  • Quarterly: Update geographic restrictions based on business expansion

3. OAuth & Integration Security

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.2
NIST 800-53 AC-6, CM-7

Description

Control which OAuth applications users can authorize and require admin approval for new app integrations. Prevent shadow IT through unconsented OAuth grants.

Rationale

Why This Matters:

  • Okta’s 7,000+ integrations create massive attack surface
  • Malicious apps can request broad OAuth scopes
  • Unconsented apps bypass security review

Attack Prevented: OAuth phishing, malicious app consent, shadow IT

ClickOps Implementation

Step 1: Restrict User-Initiated App Requests

  1. Navigate to: Applications and Resources → Self Service
  2. Under User App Requests, click Edit
  3. Uncheck Allow users to add personal apps
  4. Uncheck Allow users to add org-managed apps unless every self-service app requires admin approval
  5. Click Save

Step 2: Review Existing App Grants

  1. Navigate to: Applications and Resources → Applications
  2. For each OIDC and API Services app, open the Okta API Scopes tab
  3. Review for over-permissioned or suspicious grants
  4. Revoke unnecessary grants

Step 3: Restrict API Token Creation

  1. Navigate to: Security → API → Tokens
  2. Review existing tokens and revoke any without a current owner and purpose
  3. Require step-up authentication to create tokens: Applications and Resources → Applications → Okta Admin Console → Protected Actions → confirm Create API Token is selected
  4. SSWS tokens have no expiration-policy setting: a token expires only after 30 days without use, and that period is fixed (Okta: Create an API token) — so schedule rotation (see 3.4)

Code Implementation

Code Pack: API Script
hth-okta-3.01-oauth-consent-policies.sh View source on GitHub ↗
# List all active applications with OAuth/OIDC sign-on (a failed read stops the pack)
info "3.1 Listing active applications..."
ACTIVE_APPS=$(okta_get "/api/v1/apps?filter=status%20eq%20%22ACTIVE%22&limit=200")

TOTAL_COUNT=$(printf '%s' "${ACTIVE_APPS}" | jq 'length')
OAUTH_APPS=$(printf '%s' "${ACTIVE_APPS}" | jq '[.[] | select(.signOnMode == "OPENID_CONNECT" or .signOnMode == "OAUTH_2_0")]')
OAUTH_COUNT=$(printf '%s' "${OAUTH_APPS}" | jq 'length')

info "3.1 Total active apps: ${TOTAL_COUNT}, OAuth/OIDC apps: ${OAUTH_COUNT}"
printf '%s' "${OAUTH_APPS}" | jq -r '.[] | "  - \(.label // .name) (mode: \(.signOnMode), created: \(.created))"'
# Audit OAuth token clients on default authorization server
info "3.1 Auditing OAuth clients on default authorization server..."
AUTH_CLIENTS=$(okta_get "/api/v1/authorizationServers/default/clients")
CLIENT_COUNT=$(printf '%s' "${AUTH_CLIENTS}" | jq 'length')

if [ "${CLIENT_COUNT}" -gt 0 ]; then
  info "3.1 Found ${CLIENT_COUNT} OAuth client(s) on default auth server"
  printf '%s' "${AUTH_CLIENTS}" | jq -r '.[] | "  - \(.client_name // "unnamed") (ID: \(.client_id))"'
else
  info "3.1 No OAuth clients hold tokens from the default authorization server"
fi

3.2 Harden SCIM Provisioning Connectors

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 AC-2, IA-4

Description

Secure SCIM (System for Cross-domain Identity Management) connectors that provision/deprovision users to downstream applications. SCIM tokens enable identity manipulation across connected apps.

Rationale

Why This Matters:

  • SCIM connectors create/delete users in downstream apps
  • Compromised SCIM tokens enable backdoor account creation
  • Unlimited token validity creates persistent risk

Attack Prevented: Backdoor account creation in downstream apps via stolen SCIM tokens

Attack Scenario: Attacker steals SCIM token, creates backdoor accounts in connected SaaS apps

ClickOps Implementation

Step 1: Audit SCIM-Enabled Apps

  1. Navigate to: Applications and Resources → Applications
  2. Open each app and look for a Provisioning tab (the application list has no provisioning filter)
  3. Document all SCIM integrations

Step 2: Rotate SCIM Credentials

  1. For each SCIM-enabled app:
    • Issue a new credential (API token or service-account password) in the receiving application
    • In Okta, open the app → Provisioning tab → Settings → Integration → SCIM Connection → Edit
    • Replace the credential under Authentication Mode and click Save — Okta tests the connection on save, so a wrong credential is rejected immediately
    • Revoke the old credential in the receiving application
  2. Document credential rotation schedule (quarterly minimum)

Step 3: Limit SCIM Scope

  1. Under SCIM Connection → Supported provisioning actions, enable only the actions you need (Push New Users, Push Profile Updates, Push Groups, Import New Users and Profile Updates, Import Groups)
  2. In Settings → To App, sync only required attributes and leave Sync Password off unless required
  3. Use Push Groups only for necessary groups

Monitoring

Code Pack: Sigma Detection Rule
hth-okta-3.02-harden-scim-provisioning-connectors.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'system.scim.user.create'
            - 'system.scim.user.update'
    condition: selection
fields:
    - actor.displayName
    - target.displayName
    - target.type
    - eventType
    - client.ipAddress
    - published

3.3 Implement OAuth Application Allowlisting

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 CM-7, AC-6

Description

Restrict which third-party applications can receive OAuth grants from users. OAuth consent phishing is a growing attack vector where malicious applications request broad scopes to access organizational data through user consent flows.

Rationale

Why This Matters:

  • Over-privileged OAuth tokens from third-party integrations enable supply chain attacks
  • Users can unknowingly grant broad access to malicious applications via consent phishing
  • SaaS-to-SaaS connections create hidden trust relationships that bypass traditional security controls
  • Unsanctioned apps with broad OAuth scopes create persistent backdoors

Attack Prevented: OAuth consent phishing, supply chain compromise via over-privileged integrations, shadow IT

ClickOps Implementation

Step 1: Review Existing OAuth Grants

  1. Navigate to: Applications and Resources → Applications
  2. Open each OIDC and API Services app (the list filters only by status, not sign-on method)
  3. Click the Okta API Scopes tab
  4. Document all granted scopes — flag any with okta.users.manage, okta.apps.manage, or okta.authorizationServers.manage

Step 2: Restrict Which Apps Users Can Add

  1. Navigate to: Applications and Resources → Self Service
  2. Under User App Requests, click Edit and apply the restrictions from Section 3.1 Step 1
  3. For user consent to OAuth scopes, review each OIDC app’s consent setting and the authorization server’s scope consent settings (Step 3) — Okta has no org-wide “only pre-approved applications” switch

Step 3: Audit API Scopes for Each Application

  1. Navigate to: Security → API → Authorization Servers
  2. Select the default authorization server
  3. Click Scopes tab — review all custom scopes
  4. Click Access Policies tab — verify policies restrict token issuance to approved clients

Step 4: Create Regular Grant Review Process

  1. Export OAuth grant report monthly
  2. Revoke grants for applications no longer in use
  3. Alert on new OAuth consent events

Code Implementation

Code Pack: API Script
hth-okta-3.03-oauth-app-allowlisting.sh View source on GitHub ↗
# List all active OIDC/OAuth apps (a failed read stops the pack)
ACTIVE_APPS=$(okta_get "/api/v1/apps?filter=status%20eq%20%22ACTIVE%22&limit=200")
APP_IDS=$(printf '%s' "${ACTIVE_APPS}" | jq -r '.[] | select(.signOnMode == "OPENID_CONNECT" or .signOnMode == "OAUTH_2_0") | .id')
GRANTS=$(okta_get "/api/v1/apps/${APP_ID}/grants")
BROAD_SCOPES=$(printf '%s' "${GRANTS}" | jq -r '.[] | select(.scopeId | test("manage|write"; "i")) | .scopeId')
# List OAuth clients on default authorization server
info "3.3 Auditing default authorization server clients..."
okta_get "/api/v1/authorizationServers/default/clients" \
  | jq -r '.[] | "  - \(.client_name // "unnamed") (ID: \(.client_id))"'
Code Pack: Sigma Detection Rule
hth-okta-3.03-oauth-app-allowlisting.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'app.oauth2.consent.grant'
            - 'app.oauth2.as.consent.grant'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - target.displayName
    - debugContext.debugData.requestedScopes
    - client.ipAddress
    - published

Validation & Testing

  1. Verify admin approval is required for new app integrations
  2. Attempt to add an unauthorized OAuth application as a standard user — should require admin approval
  3. Confirm no applications have overly broad scopes (*.manage, *.write) unless justified

Monitoring & Maintenance

Detection rule: See the Sigma rule in Code Pack section 3.3 above.

Maintenance schedule:

  • Monthly: Review OAuth consent grants across all users
  • Quarterly: Audit application scopes and remove excessive permissions
  • On new integration: Require security review before OAuth grant approval

3.4 Govern Non-Human Identities (NHI)

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 IA-4, IA-5, AC-2
DISA STIG V-279689, V-279690 (V1R2, Jan 2026)

Description

Implement governance for non-human identities: service accounts, API tokens, automation accounts, and machine-to-machine (M2M) integrations. Migrate from static SSWS API tokens to OAuth 2.0 for API access. NHI compromise is a leading cause of identity-based breaches, and DISA’s Okta IDaaS STIG V1R2 now requires API tokens to be network-restricted and created under dedicated accounts.

Rationale

Why This Matters:

  • The October 2023 Okta breach was caused by a compromised service account whose credentials were saved to a personal Google profile
  • Static SSWS API tokens stay valid as long as they are used at least once every 30 days, so a token in regular use never expires on its own — a persistent access risk
  • Service accounts tied to individual admin users become orphaned when that admin leaves
  • OAuth 2.0 provides shorter token lifespans, granular scopes, and automatic key rotation vs static SSWS
  • DISA STIG V1R2 (Jan 2026) adds two API-token checks: tokens restricted to network zones (V-279689) and created under dedicated user accounts (V-279690)

Attack Prevented: Service account compromise, API token theft and replay, persistent unauthorized access via stale tokens

Real-World Incidents:

  • October 2023: Compromised service account credentials stored in personal Google profile enabled breach of Okta support system

ClickOps Implementation

Step 1: Audit All API Tokens

  1. Navigate to: Security → API → Tokens
  2. Document all active tokens:
    • Token name and purpose
    • Created by (which admin)
    • Created date
    • Last used date
    • Network restrictions (if any)
  3. Flag tokens with no activity in 90+ days for deactivation
  4. Flag tokens created by users who are no longer active

Step 2: Restrict SSWS Tokens to Network Zones

  1. A token’s network restriction is chosen when it is created: Security → API → Tokens → Create token → API calls made with this token must originate from: choose In any of the following zones: and select a specific network zone (e.g., “Corporate Network” or “Automation Servers”) instead of Any IP
  2. Re-create any existing token that allows Any IP with a zone restriction, move its consumer to the new token, then revoke the old one
  3. This limits where stolen tokens can be replayed from (DISA STIG V-279689)

Step 3: Create OAuth 2.0 Service Apps (Migration)

  1. Navigate to: Applications and Resources → Applications (or Applications and Resources → API Service Integrations)
  2. Click Create App Integration
  3. Select API Services → Next
  4. Enter App integration name: “[Service Name] API Access” and click Save — the app is created as a Service with grant type Client Credentials
  5. On the General tab, under Client Credentials, click Edit and set Client authentication to Public key / Private key (recommended) or Client secret, then save
  6. On the Okta API Scopes tab, click Grant for ONLY the minimum required scopes
  7. Token lifetime: access tokens that carry Okta API scopes (okta.*) come from the org authorization server, whose policies can’t be customized, with a lifetime fixed at one hour (Okta: OAuth for Okta service apps); the default custom authorization server’s access policies do not govern them

Step 4: Create Dedicated Service Accounts

  1. Navigate to: Directory → People
  2. Click Add person
  3. Create a dedicated service account (DISA STIG V-279690 requires API tokens to be created under dedicated accounts):
    • First name: “SVC”
    • Last name: “[Service Name]”
    • Username: “svc-[service]@yourdomain.com”
    • User type: Set to a custom “Service Account” type if available
  4. Assign minimum-required admin role (custom role preferred over built-in)
  5. Never use personal admin accounts for service/automation purposes

Step 5: Establish Token Rotation Policy

  1. Document token rotation schedule:
    • SSWS tokens (legacy): Rotate every 90 days maximum
    • OAuth 2.0 client secrets: Rotate every 180 days
    • OAuth 2.0 private keys: Rotate annually
  2. Set calendar reminders for rotation dates
  3. Include token rotation in operational runbooks

Code Implementation

Code Pack: Terraform
hth-okta-3.04-govern-non-human-identities.tf View source on GitHub ↗
# OAuth 2.0 service app using client_credentials with private_key_jwt
resource "okta_app_oauth" "service_automation" {
  label                      = "SVC - Automation API Access"
  type                       = "service"
  grant_types                = ["client_credentials"]
  response_types             = ["token"]
  token_endpoint_auth_method = "private_key_jwt"
  pkce_required              = false

  jwks {
    kid = var.service_app_public_key_kid
    kty = "RSA"
    e   = var.service_app_public_key_e
    n   = var.service_app_public_key_n
  }
}

# Grant minimum-required API scopes to the service app
resource "okta_app_oauth_api_scope" "users_read" {
  app_id = okta_app_oauth.service_automation.id
  issuer = "https://${var.okta_domain}"
  scopes = ["okta.users.read"]
}
Code Pack: API Script
hth-okta-3.04-govern-non-human-identities.sh View source on GitHub ↗
# List all active API tokens (a failed read stops the pack)
info "3.4 Listing all active API tokens..."
API_TOKENS=$(okta_get "/api/v1/api-tokens")
TOKEN_COUNT=$(printf '%s' "${API_TOKENS}" | jq 'length')
# List service applications (OAuth client_credentials)
info "3.4 Listing OAuth service applications..."
SERVICE_APPS=$(okta_get "/api/v1/apps?filter=status%20eq%20%22ACTIVE%22&limit=200" \
  | jq '[.[] | select((.settings.oauthClient.grant_types? // []) | index("client_credentials"))]')
SVC_COUNT=$(printf '%s' "${SERVICE_APPS}" | jq 'length')
Code Pack: Sigma Detection Rule
hth-okta-3.04-govern-non-human-identities.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'system.api_token.create'
            - 'system.api_token.revoke'
            - 'app.oauth2.token.grant'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - eventType
    - target.displayName
    - client.ipAddress
    - published

SSWS to OAuth 2.0 Migration Checklist

  • Inventory all active SSWS tokens and their consumers
  • Create OAuth 2.0 service app for each integration
  • Generate and distribute private keys to consuming services
  • Update consuming services to use OAuth 2.0 client credentials flow
  • Test each integration with OAuth 2.0 tokens
  • Add IP restrictions to SSWS tokens during transition (as fallback)
  • Revoke SSWS tokens after successful migration verification
  • Document new OAuth 2.0 credentials and rotation schedule

Validation & Testing

  1. Verify all API tokens have network restrictions applied
  2. Confirm no tokens are older than 90 days without documented exception
  3. Test OAuth 2.0 service app authentication using client credentials flow
  4. Verify no SSWS tokens are assigned to personal admin accounts used by humans

Monitoring & Maintenance

Detection rule: See the Sigma rule in Code Pack section 3.4 above.

Maintenance schedule:

  • Monthly: Review API token usage (flag tokens with no recent activity)
  • Quarterly: Rotate SSWS tokens and OAuth 2.0 client secrets
  • On employee departure: Audit and reassign any tokens created by departing admin
  • Annually: Rotate OAuth 2.0 private keys

3.5 Authorize AI Agents and MCP Servers with Cross App Access (XAA)

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.4, 6.8
NIST 800-53 AC-3, AC-6, IA-4, IA-5, AU-2

Description

Cross App Access (XAA) is Okta’s extension to OAuth 2.0 that lets an enterprise broker, scope, and audit the connections AI agents and Model Context Protocol (MCP) servers make to business applications, replacing the standing API keys those integrations otherwise require. It builds on the IETF Identity Assertion JWT Authorization Grant draft, which combines RFC 8693 token exchange with RFC 7523 JWT bearer authorization, so an agent receives a scoped, short-lived, centrally revocable token bound to an enterprise identity instead of a long-lived secret pasted into a configuration file. The Model Context Protocol specification has adopted the pattern as “Enterprise-Managed Authorization,” and Okta has stated Okta Integration Network availability for Workforce Identity customers beginning August 2026. (Okta announces Cross App Access partners)

Rationale

Why This Matters:

  • AI agents and MCP servers are being onboarded the way early SaaS integrations were — a human generates a long-lived API key, pastes it into a config, and nobody revisits it — which is exactly the standing-credential pattern Section 3.4 exists to eliminate
  • An agent holding a static key inherits its creator’s access indefinitely; when that person changes roles or leaves, the agent keeps working and no part of the joiner-mover-leaver process catches it
  • Keys embedded in agent configuration are routinely readable by the model’s own context, committed to repositories, or synced to unmanaged endpoints — the same failure mode as the October 2023 service account credential saved to a personal Google profile
  • XAA issues per-connection tokens scoped to specific resources with enterprise-set lifetimes, so revoking a user or an agent in Okta severs every downstream connection immediately instead of requiring a hunt for issued keys
  • Because the exchange runs through Okta, every agent-to-application call produces a System Log record attributable to an enterprise identity, turning agent activity into auditable access rather than anonymous API traffic in the destination app

Attack Prevented: Standing API key theft and replay, orphaned agent access after offboarding, unbounded agent privilege, unauditable AI-to-SaaS data movement, supply chain compromise through third-party MCP servers

Prerequisites

  • Okta Workforce Identity Cloud tenant
  • XAA entitlement confirmed with your Okta account team (OIN availability begins August 2026)
  • Completed non-human identity inventory from Section 3.4
  • Super Admin access

ClickOps Implementation

Step 1: Inventory AI Agents and MCP Servers Already in Use

  1. Navigate to: Directory → AI Agents for agents already registered, and to Applications and Resources → Applications → each app → Okta API Scopes for current grants
  2. Navigate to: Security → API → Tokens and flag every SSWS token whose consumer is an AI assistant, agent framework, or MCP server
  3. Interview engineering and operations teams about MCP servers running on developer workstations against corporate SaaS — these almost never appear in the application inventory
  4. Record for each agent: the applications it reaches, the credential type it holds, the human who created it, and the scopes granted

Step 2: Confirm XAA Availability for Your Tenant and Applications

  1. Confirm your edition and XAA entitlement with your Okta account team
  2. Navigate to: Applications and Resources → Applications → Browse App Catalog and check the Cross App Access category (and MCP Server) for an XAA-enabled integration of each target application
  3. For applications without XAA support, keep them on the OAuth 2.0 service app pattern from Section 3.4 and record the gap in your risk register

Step 3: Register the Agent as an Identity-Bound Client

  1. Navigate to: Directory → AI Agents → Register AI agent, name the agent for its function rather than the person who configured it (e.g., “MCP — Ticket Triage Agent”), and complete the wizard’s User access and authentication step
  2. For an agent that cannot use XAA, register it instead as a service app: Applications and Resources → Applications → Create App Integration → API Services → Next, enter the name, and click Save
  3. On the service app’s General tab, under Client Credentials → Edit, set Client authentication to Public key / Private key — never a shared client secret
  4. On the Okta API Scopes tab (and in the resource application’s own scopes), Grant only the scopes the agent’s task actually requires, read-only wherever the workflow permits

Step 4: Enable the Cross App Access Connection

  1. Open the resource application’s configuration and enable the Cross App Access connection for the registered agent client
  2. Assign the connection to a named group of authorized users rather than to Everyone — the agent’s effective access is bounded by the user identity it acts on behalf of
  3. Confirm the agent obtains access through token exchange and holds no application-native API key of its own
  4. Remove the superseded static key from both Okta and the downstream application

Step 5: Set Token Lifetimes and a Revocation Path

  1. Navigate to: Security → API → Authorization Servers → default → Access Policies
  2. Set the access token lifetime for agent clients to the shortest value the workflow tolerates — start at 1 hour
  3. Document the revocation path: deactivating the app integration or removing the user from the assigned group terminates the agent’s access immediately
  4. Add agent connections to the quarterly access review in Section 7.3

Time to Complete: ~1 hour per agent integration, plus inventory time

Code Implementation

Code Pack: API Script
hth-okta-3.05-authorize-ai-agents-with-xaa.sh View source on GitHub ↗
# Service apps holding a shared secret rather than a private key
SECRET_CLIENTS=$(printf '%s' "${ACTIVE_APPS}" | jq -r '.[]
  | select(((.settings.oauthClient.grant_types? // []) | index("client_credentials")) != null)
  | select(.credentials.oauthClient.token_endpoint_auth_method != "private_key_jwt")
  | "  - \(.label) (ID: \(.id), client auth: \(.credentials.oauthClient.token_endpoint_auth_method))"')
# Cross App Access connections for each OIDC app integration that has its own
# OAuth client (Okta's built-in first-party apps have no credentials.oauthClient
# and the connections endpoint rejects them)
CONNECTIONS=0
for APP_ID in $(printf '%s' "${ACTIVE_APPS}" | jq -r '.[] | select(.signOnMode == "OPENID_CONNECT" and .credentials.oauthClient != null) | .id'); do
  APP_CONNECTIONS=$(okta_get "/api/v1/apps/${APP_ID}/cwo/connections")
  COUNT=$(printf '%s' "${APP_CONNECTIONS}" | jq 'length')
  if [ "${COUNT}" -gt 0 ]; then
    printf '%s' "${APP_CONNECTIONS}" | jq -r '.[] | "  - \(.requestingAppInstanceId) -> \(.resourceAppInstanceId) (status: \(.status))"'
    CONNECTIONS=$((CONNECTIONS + COUNT))
  fi
done

Validation & Testing

  1. Every AI agent and MCP server in the inventory maps to either an XAA connection or a documented OAuth 2.0 service app — zero remain on static SSWS tokens
  2. Confirm no agent integration authenticates with a shared client secret; all use private key authentication
  3. Confirm agent scopes are read-only unless a write path is explicitly justified and documented
  4. Revoke a test agent’s group assignment and confirm its next call to the downstream application fails
  5. Review the System Log for app.oauth2.as.token.grant and token exchange events and confirm each agent call is attributable to a named enterprise identity
  6. Deactivate a test user and confirm the agent connections acting on that user’s behalf stop working

Expected result: No AI agent or MCP server holds a standing credential; every agent-to-application connection is scoped, time-limited, attributable to an enterprise identity, and revocable from Okta.

Monitoring & Maintenance

Maintenance schedule:

  • Monthly: Review new agent registrations and confirm each went through security review before receiving scopes
  • Quarterly: Re-certify agent connections as part of the access review in Section 7.3
  • On offboarding: Confirm the departing user’s agent connections are terminated, not just their interactive access
  • On MCP server addition: Treat a third-party MCP server as a third-party integration and run it through Section 6.1 risk assessment first

Compliance Mappings

Framework Control Requirement
CIS Controls v8 5.4 Restrict privileges to dedicated accounts, including non-human identities
CIS Controls v8 6.8 Define and maintain role-based access control for service integrations
NIST 800-53 AC-3 Access enforcement through brokered, scoped authorization
NIST 800-53 AC-6 Least privilege applied to machine and agent identities
NIST 800-53 IA-4 Identifier management for non-human identities
NIST 800-53 IA-5 Authenticator management with short-lived, revocable tokens
NIST 800-53 AU-2 Auditable event records for every agent-to-application call
SOC 2 CC6.1 Logical access to systems restricted to authorized identities

4. Session Management

4.1 Configure Session Timeouts

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 AC-12, SC-10
DISA STIG V-273186, V-273187, V-273203

Description

Set session timeouts appropriate to risk level. Reduce maximum session lifetime and enforce re-authentication for sensitive applications.

Rationale

Why This Matters:

  • Long sessions increase window for session hijacking
  • October 2023 breach exploited long-lived session cookies
  • Idle timeouts reduce exposure from abandoned sessions

Attack Prevented: Session hijacking via long-lived or abandoned sessions, stolen session cookie replay

Specification Requirements

Setting L1 (Crawl) L2 (Walk) L3/DISA STIG
Max session lifetime 12 hours 8 hours 18 hours
Max idle time 1 hour 30 minutes 15 minutes
Admin Console idle time 30 minutes 15 minutes 15 minutes
Persistent sessions Optional Disabled Disabled

ClickOps Implementation

Step 1: Configure Global Session Policy

  1. Navigate to: Security → Global Session Policy
  2. Select the Default Policy
  3. Click Add rule (create a custom rule at Priority 1, not the “Default Rule”)
  4. Configure settings per the specification requirements table above

Step 2: Configure Admin Console Session Timeout

  1. Navigate to: Applications and Resources → Applications → Okta Admin Console
  2. Click the Sign On tab
  3. Under “Okta Admin Console session”, click Edit and set:
    • Maximum app session idle time: 15 minutes (L2/L3)
  4. Click Save

Step 3: Require Re-Authentication for Sensitive Apps For sensitive apps (PAM, admin consoles, financial systems), re-authentication is set in the app’s authentication policy — an app’s Sign On tab has no session-lifetime field:

  1. Navigate to: Security → Authentication Policies → App sign-in → the policy assigned to the app
  2. Click Actions next to its rule → Edit
  3. Set Prompt for authentication to Every time user signs in to resource (or to a time-based interval of 2 hours at most)
  4. Click Save

Code Implementation

Code Pack: Terraform
hth-okta-4.01-configure-session-timeouts.tf View source on GitHub ↗
# Global session policy with hardened timeout values
resource "okta_policy_signon" "session_timeouts" {
  name        = "Hardened Session Timeouts"
  status      = "ACTIVE"
  description = "Session timeout configuration per HTH hardening guide"
  priority    = 3
}

resource "okta_policy_rule_signon" "session_timeout_rule" {
  policy_id          = okta_policy_signon.session_timeouts.id
  name               = "Enforce Session Timeouts"
  status             = "ACTIVE"
  priority           = 1
  access             = "ALLOW"
  mfa_required       = true
  mfa_prompt         = "SESSION"
  session_lifetime   = var.session_max_lifetime_minutes
  session_idle       = var.session_max_idle_minutes
  session_persistent = false
}
Code Pack: API Script
hth-okta-4.01-configure-session-timeouts.sh View source on GitHub ↗
# Get global session policies and report current settings (a failed read stops the pack)
POLICIES=$(okta_get "/api/v1/policies?type=OKTA_SIGN_ON")
POLICY_COUNT=$(printf '%s' "${POLICIES}" | jq 'length')

if [ "${POLICY_COUNT}" -eq 0 ]; then
  fail "4.1 No global session policies returned -- every Okta org has a Default Policy"
  increment_failed
  summary
fi

for POLICY_ID in $(printf '%s' "${POLICIES}" | jq -r '.[].id'); do
  POLICY_NAME=$(printf '%s' "${POLICIES}" | jq -r --arg id "${POLICY_ID}" '.[] | select(.id == $id) | .name')
  info "4.1 Reviewing session policy '${POLICY_NAME}' (${POLICY_ID})..."
  okta_get "/api/v1/policies/${POLICY_ID}/rules" \
    | jq -r '.[] | "  - Rule: \(.name), MaxLifetime: \(.actions.signon.session.maxSessionLifetimeMinutes // "default")min, MaxIdle: \(.actions.signon.session.maxSessionIdleMinutes // "default")min, Persistent: \(.actions.signon.session.usePersistentCookie // "default")"'
done
# Okta Admin Console session idle time and lifetime
okta_get "/api/v1/first-party-app-settings/admin-console" \
  | jq -r '"  - Admin Console: idle \(.sessionIdleTimeoutMinutes)min, max lifetime \(.sessionMaxLifetimeMinutes)min"'

4.2 Disable Session Persistence

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 SC-23
DISA STIG V-273206

Description

Disable “Remember Me” and persistent session features that increase session hijacking risk. Persistent global session cookies allow sessions to survive browser restarts, which extends the window for session hijacking.

Rationale

Why This Matters:

  • Persistent session cookies survive browser restarts and stay valid for extended periods, lengthening the window in which a stolen cookie can be replayed
  • “Remember me” and persistent MFA-device features let an attacker who steals a session token bypass re-authentication entirely
  • The October 2023 breach demonstrated that long-lived session cookies extracted from HAR files grant direct access without credentials or MFA
  • Ending sessions with the browser limits exposure on shared, lost, or compromised devices

Attack Prevented: Session cookie theft and replay, session hijacking, persistent access from compromised devices

ClickOps Implementation

  1. Navigate to: Security → Global Session Policy
  2. Select the Default Policy
  3. Click Add rule (create a custom rule at Priority 1)
  4. Configure:
    • Okta global session cookies persist across browser sessions: Disable
    • Multifactor authentication (MFA) is: Required, with Users will be prompted for MFA: At every sign in (not “When signing in with a new device cookie”)
  5. Navigate to: Security → General → Organization Security → Edit
  6. Set Show option to stay signed in before users sign in to Not Enabled and click Save; also leave Option to stay signed in off in each app sign-in policy rule

Code Implementation

Code Pack: Terraform
hth-okta-4.02-disable-session-persistence.tf View source on GitHub ↗
# Global signon policy that disables persistent sessions
# Prevents session cookies from surviving browser restarts
resource "okta_policy_signon" "disable_session_persistence" {
  count = var.profile_level >= 2 ? 1 : 0

  name        = "Disable Session Persistence"
  status      = "ACTIVE"
  description = "Disables Remember Me and persistent session cookies to reduce session hijacking risk"
  priority    = 2
}

# Rule enforcing non-persistent sessions with strict timeouts
resource "okta_policy_rule_signon" "no_persistent_sessions" {
  count = var.profile_level >= 2 ? 1 : 0

  policy_id          = okta_policy_signon.disable_session_persistence[0].id
  name               = "No Persistent Sessions"
  status             = "ACTIVE"
  priority           = 1
  access             = "ALLOW"
  mfa_required       = true
  mfa_prompt         = "SESSION"
  session_lifetime   = 480
  session_idle       = 30
  session_persistent = false
}
Code Pack: API Script
hth-okta-4.02-disable-session-persistence.sh View source on GitHub ↗
# A failed read stops the pack -- an unread rule is never reported as compliant
POLICIES=$(okta_get "/api/v1/policies?type=OKTA_SIGN_ON")

persistent_found=false

for POLICY_ID in $(printf '%s' "${POLICIES}" | jq -r '.[].id'); do
  RULES=$(okta_get "/api/v1/policies/${POLICY_ID}/rules")
  PERSISTENT=$(printf '%s' "${RULES}" | jq '[.[] | select(.actions.signon.session.usePersistentCookie == true)] | length')

  if [ "${PERSISTENT}" -gt 0 ]; then
    persistent_found=true
    POLICY_NAME=$(printf '%s' "${POLICIES}" | jq -r --arg id "${POLICY_ID}" '.[] | select(.id == $id) | .name')
    warn "4.2 Found ${PERSISTENT} rule(s) with persistent sessions in policy '${POLICY_NAME}' (${POLICY_ID})"
  fi
done

4.3 Configure Admin Session Security

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 SC-23, AC-12

Description

Harden admin sessions with IP binding, Protected Actions, and per-sign-in re-authentication for the Admin Console. These controls prevent session hijacking by invalidating admin sessions when network characteristics change, and require step-up authentication before critical operations.

Rationale

Why This Matters:

  • The October 2023 breach demonstrated that stolen admin session tokens can be replayed from any network
  • Admin Session ASN Binding invalidates sessions when the Autonomous System Number changes (e.g., attacker replays from a different ISP)
  • Admin Session IP Binding is more restrictive — invalidates on any IP change
  • Protected Actions require step-up authentication before high-impact operations like creating IdPs or resetting MFA factors
  • These are post-breach product enhancements specifically designed to prevent session hijacking

Attack Prevented: Admin session hijacking, stolen session token replay, unauthorized critical operations

Real-World Incidents:

  • October 2023: Stolen HAR file session tokens replayed from attacker infrastructure to access admin consoles

ClickOps Implementation

Step 1: Understand Admin Session ASN Binding Okta binds admin sessions to the network (ASN) they started on — its admin-account guidance still describes this as a protection — but current consoles expose no ASN setting to verify or change. Rely on IP binding (Step 2) for a control you can see.

Step 2: Enable Admin Session IP Binding (Recommended for L2+)

  1. Navigate to: Security → General
  2. In Organization Security, click Edit
  3. Set IP binding for admin console to Enabled
  4. Click Save

Note: IP binding may cause disruptions for admins on dynamic IP addresses or mobile networks. Test with a pilot group before enforcing broadly.

Step 3: Enable Protected Actions

  1. Navigate to: Applications and Resources → Applications → Okta Admin Console → Protected Actions
  2. Click Edit next to Protected actions
  3. Select the operations that require step-up authentication, for example:
    • ☑ Create identity providers
    • ☑ Modify identity providers
    • ☑ Reset authenticators for super admins
    • ☑ Update any authentication policy/app sign-on policy
    • ☑ Update global session policy/Okta sign-on policy
    • ☑ Assign and revoke admin role (excluding super admin role)
    • ☑ Assign and revoke super admin role
    • ☑ Create API Token
  4. Set Authentication required every to 1 minute(s)
  5. Click Save configuration. The step-up factor comes from the Okta Admin Console authentication policy (Step 4), so make that policy phishing-resistant

Step 4: Require Re-Authentication for Every Admin Console Sign-In

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. Select the Okta Admin Console policy
  3. Click Actions next to the active rule → Edit:
    • Possession factor constraints are: Phishing resistant
    • Prompt for authentication: Every time user signs in to resource
  4. Click Save

Code Implementation

Code Pack: API Script
hth-okta-4.03-admin-session-security.sh View source on GitHub ↗
# The Okta Admin Console is the app named "saasure"; its app sign-in policy is
# linked from the app object (a failed read stops the pack)
ADMIN_APP=$(okta_get "/api/v1/apps?filter=name%20eq%20%22saasure%22" | jq -c '.[0] // empty')
if [ -z "${ADMIN_APP}" ]; then
  fail "4.3 Okta Admin Console app (saasure) not returned -- cannot audit its policy"
  increment_failed
  summary
fi
POLICY_ID=$(printf '%s' "${ADMIN_APP}" | jq -r '._links.accessPolicy.href | split("/") | last')
RULES=$(okta_get "/api/v1/policies/${POLICY_ID}/rules")

# ALLOW rules that do not require a phishing-resistant factor, or that let an
# earlier sign-in satisfy the Admin Console (reauthenticateIn other than PT0S)
WEAK_RULES=$(printf '%s' "${RULES}" | jq -r '
  .[] | select(.status == "ACTIVE" and .actions.appSignOn.access == "ALLOW")
  | select(
      ([.actions.appSignOn.verificationMethod.constraints[]?.possession.phishingResistant] | index("REQUIRED") | not)
      or (.actions.appSignOn.verificationMethod.reauthenticateIn // "" ) != "PT0S")
  | "  - \(.name): phishing-resistant=\([.actions.appSignOn.verificationMethod.constraints[]?.possession.phishingResistant] | join(",")), reauthenticateIn=\(.actions.appSignOn.verificationMethod.reauthenticateIn // "unset")"')
Code Pack: Sigma Detection Rules (2)
hth-okta-4.03-admin-session-security-b.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'system.protected_action.challenge'
            - 'system.protected_action.success'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - eventType
    - target.displayName
    - client.ipAddress
    - published

hth-okta-4.03-admin-session-security.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'user.session.invalidate'
    filter_binding:
        debugContext.debugData.reason: 'ADMIN_SESSION_BINDING'
    condition: selection and filter_binding
fields:
    - actor.displayName
    - actor.alternateId
    - client.ipAddress
    - debugContext.debugData.reason
    - outcome.result
    - published

The pack above audits Step 4 read-only. Steps 2 and 3 are console-only: the Okta Management API documents no endpoint for admin console IP binding or Protected Actions (Okta Management API, spec 2026.08.4, checked 2026-09-24).

Validation & Testing

  1. Verify IP binding is active: Security → General → Organization Security → IP binding for admin console
  2. Verify the selected operations on the Okta Admin Console app’s Protected Actions tab
  3. Test Protected Actions: Attempt to modify an IdP — should prompt for step-up authentication
  4. Test session invalidation: Log in as admin, change network (e.g., switch from WiFi to VPN) — session should be invalidated if IP binding is enabled

Monitoring & Maintenance

Detection rules: See the Sigma rules in Code Pack section 4.3 above for session-binding and Protected Actions events.

Maintenance schedule:

  • Monthly: Review Protected Actions audit log for any failures or unusual patterns
  • Quarterly: Review IP binding exceptions for admins on dynamic networks

5. Monitoring & Detection

5.1 Enable Comprehensive System Logging

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 AU-2, AU-3, AU-6
DISA STIG V-273202 (HIGH)

Description

Configure Okta System Log forwarding to SIEM with comprehensive event capture for security monitoring and incident response.

Rationale

Why This Matters:

  • Without centralized log forwarding, security teams cannot detect authentication anomalies, policy tampering, or account takeover in time to respond
  • Okta retains System Log data for a limited window — streaming to a SIEM preserves the evidence needed for forensic investigation and compliance retention
  • Correlating Okta events with other telemetry surfaces multi-stage attacks (impossible travel, factor enrollment, privilege change) that are invisible in isolation
  • DISA STIG V-273202 (HIGH) and most audit frameworks require centralized, tamper-evident audit logging of identity events

Attack Prevented: Undetected account takeover, log tampering, delayed incident response, evidence loss

ClickOps Implementation

Step 1: Configure Log Streaming

  1. Navigate to: Reports → Log Streaming
  2. Click Add Log Stream
  3. Select integration type and click Next:
    • AWS EventBridge - For AWS-based SIEM solutions
    • Splunk Cloud - For Splunk deployments
  4. Complete the required configuration fields
  5. Click Save and verify the connection is Active

Step 2: Alternative - Okta Log API Integration If your SIEM is not directly supported:

  1. Preferred: navigate to Applications and Resources → Applications → Create App Integration → API Services → Next, save the app, Grant only okta.logs.read on its Okta API Scopes tab, and have the SIEM authenticate with client credentials
  2. If an SSWS token is unavoidable, create it at Security → API → Tokens while signed in as a Read-only Administrator — a token has no permissions of its own and inherits its creator’s admin role
  3. Configure your SIEM to pull logs via the System Log API endpoint

Step 3: Create Alert Rules (via SIEM)

Code Implementation

Code Pack: API Script
hth-okta-5.01-comprehensive-logging.sh View source on GitHub ↗
# Verify System Log API is accessible and returning events (a failed read stops the pack)
info "5.1 Testing System Log API access..."
LOG_RESPONSE=$(okta_get "/api/v1/logs?limit=1")
LOG_COUNT=$(printf '%s' "${LOG_RESPONSE}" | jq 'length')
# Check for log streaming integrations
info "5.1 Checking log streaming configuration..."
LOG_STREAMS=$(okta_get "/api/v1/logStreams")
STREAM_COUNT=$(printf '%s' "${LOG_STREAMS}" | jq 'length')
ACTIVE_STREAMS=$(printf '%s' "${LOG_STREAMS}" | jq '[.[] | select(.status == "ACTIVE")] | length')
Code Pack: Sigma Detection Rules (3)
hth-okta-5.01-comprehensive-logging-b.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'user.session.start'
        outcome.result: 'FAILURE'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - client.ipAddress
    - outcome.result
    - outcome.reason
    - published

hth-okta-5.01-comprehensive-logging-c.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'user.authentication.sso'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - client.ipAddress
    - client.geographicalContext.country
    - client.geographicalContext.city
    - published

hth-okta-5.01-comprehensive-logging.yml View source on GitHub ↗
detection:
    selection:
        eventType|startswith: 'system.role'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - target.displayName
    - eventType
    - client.ipAddress
    - published

5.2 Configure ThreatInsight

Profile Level: L1 (Crawl)

Description

Enable Okta ThreatInsight to automatically block authentication from known-malicious IPs based on Okta’s threat intelligence.

Rationale

Why This Matters:

  • ThreatInsight draws on signals aggregated across Okta’s customer base to identify IPs actively conducting credential-based attacks
  • Setting the action to log and enforce stops authentication attempts from known-malicious sources before they ever reach a password or MFA check
  • Network-level blocking of attacker infrastructure reduces the volume of credential-stuffing and password-spray traffic the tenant must absorb
  • It is a low-effort control requiring no additional credentials that adds a proactive defensive layer in front of every login

Attack Prevented: Credential stuffing, password spraying, brute-force attempts from known-malicious IPs

ClickOps Implementation

  1. Navigate to: Security → General
  2. In Okta ThreatInsight settings, click Edit:
    • Action: Log and enforce security based on threat level
    • Exempt Zones: add only trusted network zones (e.g., scanner or test IP zones)
  3. Click Save

Note: Free-trial editions run ThreatInsight in limited capacity, as the console states.

Code Implementation

Code Pack: Terraform
hth-okta-5.02-configure-threatinsight.tf View source on GitHub ↗
# Log and enforce ThreatInsight (console: "Log and enforce security based on threat level")
resource "okta_threat_insight_settings" "threatinsight" {
  action           = "block"
  network_excludes = var.threatinsight_exempt_zone_ids
}
Code Pack: API Script
hth-okta-5.02-configure-threatinsight.sh View source on GitHub ↗
# Set ThreatInsight to block, keeping any exempt zones already configured
info "5.2 Setting ThreatInsight to block mode..."
NEW_CONFIG=$(printf '%s' "${THREAT_CONFIG}" | jq '{action: "block", excludeZones: (.excludeZones // [])}')
if okta_post "/api/v1/threats/configuration" "${NEW_CONFIG}" > /dev/null; then
  pass "5.2 ThreatInsight set to block mode"
  increment_applied
else
  fail "5.2 Failed to configure ThreatInsight"
  increment_failed
fi

5.3 Enable Identity Threat Protection (ITP)

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 SI-4, RA-5

Description

Enable Identity Threat Protection with Okta AI for continuous post-authentication risk evaluation. Unlike traditional MFA (authentication-time only), ITP evaluates risk signals during active sessions and can automatically terminate sessions, require step-up MFA, or trigger Workflows responses in real-time.

Rationale

Why This Matters:

  • Traditional authentication is a point-in-time check — once past MFA, an attacker has free access until the session expires
  • ITP continuously evaluates risk signals: session anomalies, impossible travel, credential compromise intelligence
  • Aligns with NIST 800-63-4’s Digital Identity Risk Management (DIRM) framework for continuous risk evaluation
  • Can automatically respond to detected threats without human intervention

Attack Prevented: Session hijacking detected post-authentication, compromised credential use, anomalous session behavior

Prerequisites

  • Okta Identity Threat Protection license (add-on to Okta Identity Engine)
  • Super Admin access
  • SIEM integration configured (to receive ITP events)

ClickOps Implementation

Step 1: Enable Identity Threat Protection

  1. Navigate to: Security → Identity Threat Protection
  2. Click Enable ITP
  3. Review the default risk policies

Step 2: Configure Risk Policies

  1. Navigate to: Security → Identity Threat Protection → Policies
  2. Configure response actions for each risk level:
Risk Level Recommended Action
Low Log only
Medium Require step-up MFA
High Terminate session immediately
Critical Terminate session + lock account
  1. Click Save

Step 3: Configure Session Risk Evaluation

  1. Navigate to: Security → Authentication Policies → App sign-in
  2. Edit rules to include: Evaluate risk with ITP = Enabled
  3. Set re-authentication triggers based on risk score changes

Step 4: Integrate with Okta Workflows (Optional)

  1. Navigate to: Workflow → Workflows console (opens Okta Workflows)
  2. Create a flow triggered by the ITP risk event
  3. Configure automated response actions:
    • Send Slack/Teams alert to security team
    • Create ticket in ITSM
    • Revoke active sessions for affected user
    • Add source IP to dynamic blocklist

Code Implementation

Identity Threat Protection’s policies are read-only in the Okta Management API (ENTITY_RISK and POST_AUTH_SESSION policy types), so the pack below is detection content: Sigma rules for ITP events.

Code Pack: Sigma Detection Rule
hth-okta-5.03-enable-identity-threat-protection.yml View source on GitHub ↗
detection:
    selection_event:
        eventType:
            - 'security.threat.detected'
            - 'security.session.risk_change'
    selection_risk:
        debugContext.debugData.riskLevel:
            - 'HIGH'
            - 'CRITICAL'
    condition: selection_event and selection_risk
fields:
    - actor.displayName
    - client.ipAddress
    - outcome.result
    - debugContext.debugData.riskLevel
    - debugContext.debugData.riskReasons
    - published

5.4 Configure Behavior Detection Rules

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 SI-4, AC-7

Description

Configure Okta’s Behavior Detection to identify anomalous user behavior patterns and trigger adaptive authentication responses. Detection types include new device, new location, new IP, velocity anomalies (impossible travel), and IP reputation.

Rationale

Why This Matters:

  • Behavioral analytics detect account compromise that static policies miss
  • New device/location from an existing user may indicate credential theft
  • Impossible travel (logging in from two distant locations within minutes) is a strong indicator of token replay
  • Risk-based authentication adapts security requirements to threat level

Attack Prevented: Account takeover via stolen credentials, session replay from anomalous locations, impossible travel attacks

ClickOps Implementation

Step 1: Review Behavior Detection Rules

  1. Navigate to: Security → Behavior Detection
  2. Confirm the behaviors you rely on are Active — New Device, New IP, New City, New Country, and Velocity (impossible travel). A behavior only defines what “new” means (its edit dialog holds Behavior name and Evaluate against past N Authentications); it has no action of its own
  3. Add any missing behavior with Add behavior (Location, IP, Device, Velocity, or ASN)

Step 2: Enforce Responses in the Global Session Policy

  1. Navigate to: Security → Global Session Policy → select the policy → Add rule
  2. Set the response per behavior with the Behavior is condition:
Behavior Type Recommended Response (Global Session Policy rule)
New Device Multifactor authentication (MFA) is: Required
New IP Multifactor authentication (MFA) is: Required
New City Multifactor authentication (MFA) is: Required
New State No rule (log only)
New Country Access is: Denied
Velocity (impossible travel) Access is: Denied
  1. Click Create rule for each

Step 3: Use Risk in App Sign-In Policies

  1. Navigate to: Security → Authentication Policies → App sign-in → the primary user-facing policy
  2. Add one rule per risk level with the Risk is condition:
    • Risk is: High → Access is: Denied
    • Risk is: Medium → User must authenticate with: Any 2 factor types, Possession factor constraints are: Phishing resistant
    • Risk is: Low → the policy’s normal rules apply
  3. Click Save

Code Implementation

Code Pack: Terraform
hth-okta-5.04-behavior-detection.tf View source on GitHub ↗
# Behavior detection rule for new location sign-on
resource "okta_behavior" "new_location" {
  count = var.profile_level >= 2 ? 1 : 0

  name                      = "New Location Sign-On"
  type                      = "ANOMALOUS_LOCATION"
  status                    = "ACTIVE"
  number_of_authentications = 3
  location_granularity_type = "CITY"
}

# Behavior detection rule for new device
resource "okta_behavior" "new_device" {
  count = var.profile_level >= 2 ? 1 : 0

  name                      = "New Device Sign-On"
  type                      = "ANOMALOUS_DEVICE"
  status                    = "ACTIVE"
  number_of_authentications = 3
}
Code Pack: API Script
hth-okta-5.04-behavior-detection.sh View source on GitHub ↗
# List all configured behavior detection rules (a failed read stops the pack)
info "5.4 Listing current behavior detection rules..."
BEHAVIORS=$(okta_get "/api/v1/behaviors")
BEHAVIOR_COUNT=$(printf '%s' "${BEHAVIORS}" | jq 'length')
info "5.4 Creating new country detection rule..."
if okta_post "/api/v1/behaviors" '{
  "name": "New Country Detection",
  "type": "ANOMALOUS_LOCATION",
  "status": "ACTIVE",
  "settings": {
    "granularity": "COUNTRY",
    "maxEventsUsedForEvaluation": 50
  }
}' > /dev/null; then
  pass "5.4 New Country Detection behavior rule created"
else
  fail "5.4 Failed to create the New Country Detection behavior rule"
  increment_failed
  summary
fi
Code Pack: Sigma Detection Rule
hth-okta-5.04-behavior-detection.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'security.behavior_detection.triggered'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - client.ipAddress
    - client.geographicalContext.city
    - client.geographicalContext.country
    - debugContext.debugData.behaviors
    - outcome.result
    - published

Validation & Testing

  1. Verify all behavior detection rules are active: Security → Behavior Detection
  2. Test new device detection: Log in from an unrecognized browser — should trigger MFA challenge
  3. Review risk score evaluation: Check system log for security.behavior_detection.triggered events

Monitoring & Maintenance

Detection rule: See the Sigma rule in Code Pack section 5.4 above.


5.5 Monitor for Cross-Tenant Impersonation

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 SI-4, AU-6

Description

Monitor for cross-tenant impersonation attacks where an adversary with admin access configures a malicious Identity Provider (IdP) to impersonate any user without credentials or MFA. This is a high-impact, low-volume attack that should trigger immediate investigation.

Rationale

Why This Matters:

  • An attacker with admin access can create a malicious external IdP
  • They then configure routing rules to direct authentication through the malicious IdP
  • This allows impersonation of ANY user without knowing their credentials or MFA
  • The attack leaves traces in system logs but is difficult to detect without specific monitoring
  • IdP lifecycle events are high-impact but low-volume — ideal for alerting

Attack Prevented: Cross-tenant impersonation via malicious IdP configuration, unauthorized federation trust establishment

Real-World Context:

  • Obsidian Security Research: Documented this technique as a post-compromise persistence mechanism used against Okta customers

ClickOps Implementation

Step 1: Restrict IdP Configuration Permissions

  1. Navigate to: Security → Administrators
  2. Review all users with admin roles that include IdP management permissions
  3. Limit IdP configuration capability to the absolute minimum number of administrators
  4. Create a custom admin role WITHOUT IdP management if possible:
    • Navigate to: Security → Administrators → Roles → Create new role
    • Leave unchecked Identity provider → Manage identity providers and Policies → Manage policies (API permission ids okta.idps.manage and okta.policies.manage); limit which policies a role can touch with the resource set assigned alongside it
  5. Reassign administrators to the restricted role

Step 2: Audit Existing Identity Providers

  1. Navigate to: Security → Identity Providers
  2. Document all configured IdPs:
    • Name, type, status, created date, created by
  3. Flag any IdPs that are unfamiliar or recently created
  4. Verify each IdP has a legitimate business purpose

Step 3: Audit Routing Rules

  1. Navigate to: Security → Identity Providers → Routing Rules
  2. Review all routing rules:
    • Verify each rule routes to a legitimate IdP
    • Check for overly broad conditions (e.g., “all users” routing to an external IdP)
    • Flag any recently created or modified rules

Step 4: Create SIEM Alerts for IdP Lifecycle Events Configure alerts in your SIEM for these system log events:

  • system.idp.lifecycle.create — New IdP created
  • system.idp.lifecycle.update — IdP configuration modified
  • system.idp.lifecycle.activate — IdP activated
  • system.idp.lifecycle.deactivate — IdP deactivated
  • policy.lifecycle.create / policy.lifecycle.update (where policy type = IDP_DISCOVERY) — Routing rule changes

Code Implementation

Code Pack: API Script
hth-okta-5.05-cross-tenant-impersonation.sh View source on GitHub ↗
# Audit all configured identity providers (a failed read stops the pack --
# an unread IdP list is never reported as "no IdPs")
info "5.5 Listing all configured identity providers..."
IDPS=$(okta_get "/api/v1/idps")
IDP_COUNT=$(printf '%s' "${IDPS}" | jq 'length')
# Audit IDP discovery (routing) policies
info "5.5 Auditing IDP discovery (routing) policies..."
IDP_POLICIES=$(okta_get "/api/v1/policies?type=IDP_DISCOVERY")
IDP_POLICY_COUNT=$(printf '%s' "${IDP_POLICIES}" | jq 'length')

if [ "${IDP_POLICY_COUNT}" -gt 0 ]; then
  info "5.5 Found ${IDP_POLICY_COUNT} IDP discovery policy/policies:"
  printf '%s' "${IDP_POLICIES}" | jq -r '.[] | "  - \(.name) (status: \(.status), lastUpdated: \(.lastUpdated))"'

  # Get rules for each IDP discovery policy
  for POLICY_ID in $(printf '%s' "${IDP_POLICIES}" | jq -r '.[].id'); do
    info "5.5 Routing rules for policy ${POLICY_ID}:"
    okta_get "/api/v1/policies/${POLICY_ID}/rules" | jq -r '.[] | "    - Rule: \(.name)"'
  done
fi
# Search system log for recent IdP lifecycle events (last 7 days)
info "5.5 Checking for recent IdP lifecycle events (last 7 days)..."
SINCE=$(date -d '7 days ago' -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null \
  || date -v-7d -u +%Y-%m-%dT%H:%M:%S.000Z)

IDP_EVENTS=$(okta_get "/api/v1/logs?filter=eventType+sw+%22system.idp.lifecycle%22&since=${SINCE}")
EVENT_COUNT=$(printf '%s' "${IDP_EVENTS}" | jq 'length')

if [ "${EVENT_COUNT}" -gt 0 ]; then
  warn "5.5 Found ${EVENT_COUNT} IdP lifecycle event(s) in the last 7 days -- INVESTIGATE IMMEDIATELY"
  printf '%s' "${IDP_EVENTS}" | jq -r '.[] | "  - \(.eventType): \(.actor.displayName) -> \(.target[0].displayName // "unknown") at \(.published)"'
else
  pass "5.5 No IdP lifecycle events in the last 7 days"
fi
Code Pack: Sigma Detection Rules (2)
hth-okta-5.05-cross-tenant-impersonation-b.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'policy.lifecycle.create'
            - 'policy.lifecycle.update'
    filter_policy_type:
        debugContext.debugData.policyType: 'IDP_DISCOVERY'
    condition: selection and filter_policy_type
fields:
    - actor.displayName
    - target.displayName
    - client.ipAddress
    - published

hth-okta-5.05-cross-tenant-impersonation.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'system.idp.lifecycle.create'
            - 'system.idp.lifecycle.activate'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - target.displayName
    - target.type
    - client.ipAddress
    - published

Validation & Testing

  1. Verify IdP management is restricted to minimum necessary administrators
  2. Confirm all existing IdPs have documented business justification
  3. Verify SIEM alerts are configured for system.idp.lifecycle.* events
  4. Test alert: Create a test IdP in a sandbox tenant and verify alert fires

Monitoring & Maintenance

SIEM alert rules (CRITICAL – investigate immediately): See the Sigma rules in Code Pack section 5.5 above for IdP creation and routing rule modification detections.

Maintenance schedule:

  • Weekly: Review IdP configuration and routing rules for unauthorized changes
  • Monthly: Verify SIEM alerts for IdP events are functioning (test with log injection)
  • On any alert fire: Immediately investigate — this is a high-severity indicator

5.6 Run HealthInsight Security Reviews

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 CA-7, RA-5

Description

Run Okta HealthInsight regularly to assess your tenant’s security posture against Okta’s built-in security task recommendations (18 on a current Identity Engine org). HealthInsight reports the share of tasks completed and links each open task to the setting that fixes it.

Rationale

Why This Matters:

  • HealthInsight is free and built into every Okta admin console — no additional license needed
  • Provides automated detection of common security misconfigurations
  • Serves as a baseline security checklist aligned with Okta’s own best practices
  • Posture score tracking over time demonstrates continuous improvement for auditors

Attack Prevented: Exploitation of common tenant misconfigurations left undetected between reviews

ClickOps Implementation

Step 1: Access HealthInsight

  1. Navigate to: Security → HealthInsight
  2. Review the completion summary (“N% — X of Y tasks completed”) and the Incomplete, Complete, and Dismissed tabs

Step 2: Review All 18 Tasks

# HealthInsight Task Guide Section
1 Limit the share of admins holding super admin privileges 1.2, 7.3
2 Password policy strength (minimum length 12, history 24, minimum age, lockout, common passwords restricted) 1.4, 1.5
3 A block-listed network zone exists 2.1, 2.3
4 Okta Admin Console authentication requirement is strong 1.1, 4.3
5 ThreatInsight blocks login attempts from suspicious IPs 5.2
6 Password Changed notifications are enabled 1.11
7 Session lifetime is under 2 hours in all policies 4.1
8 Suspicious activity reporting for end users is enabled 1.11
9 New Sign-On notifications are enabled 1.11
10 Authenticator Enrollment notifications are enabled 1.11
11 Authenticator Reset notifications are enabled 1.11
12 SAML authentication is enabled for all supported apps 6.2
13 Sign-in failures from Tor anonymizer proxies 2.3
14 Authenticators are required in all MFA enrollment policies 1.10
15 MFA requirements do not conflict with Behavior Detection “New Device” 5.4
16 MFA lifetime is shorter than the session expiration 4.1
17 Risk score is evaluated for each request 5.4
18 Content Security Policy is enforced for all brands —

Step 3: Remediate Incomplete Tasks

  1. For each task on the Incomplete tab:
    • Use its Go to … link to open the setting that fixes it
    • Apply the fix; the task moves to Complete on its own once HealthInsight’s check passes
    • Dismiss a task only with a documented reason (risk acceptance)
  2. Target: all 18 tasks complete, or dismissed with a documented reason

Step 4: Schedule Regular Reviews

  1. Set a monthly calendar reminder to review HealthInsight
  2. Document posture score in your security metrics dashboard
  3. Include HealthInsight review in your quarterly security review process

Automation: ClickOps only — Okta exposes no write interface for this setting: the Okta Management API documents no HealthInsight endpoint (Okta Management API, spec 2026.08.4, checked 2026-09-24). The settings each task checks are automated in the sections the table above maps them to.

Validation & Testing

  1. Navigate to Security → HealthInsight and verify it loads
  2. Document current posture score as baseline
  3. Verify all 18 tasks have been reviewed
  4. Remediate any Incomplete tasks and confirm they move to Complete

Monitoring & Maintenance

Maintenance schedule:

  • Monthly: Run HealthInsight review, remediate new findings
  • Quarterly: Report posture score to security leadership
  • After configuration changes: Re-run HealthInsight to verify no regression

5.7 Deploy Identity Security Posture Management (ISPM)

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.3, 6.2
NIST 800-53 CA-7, RA-5, AC-2(3), AC-6(7)

Description

Identity Security Posture Management is Okta’s native posture module that continuously scans the identity graph and reports exploitable weaknesses rather than checking a fixed list of tenant settings. It surfaces shadow admin accounts and permissions, dormant identities unused for 90 days or more, access points without MFA including local accounts, permission creep where users hold more access than their current role requires, and offboarded users who still retain active access. Coverage extends past Okta itself into Microsoft Entra ID (still called Azure Active Directory on Okta’s product page) and Microsoft 365, AWS, and Salesforce. (Okta Identity Security Posture Management)

Rationale

Why This Matters:

  • HealthInsight (Section 5.6) checks a fixed list of tenant settings and answers “is Okta configured correctly” — it cannot see that a user accumulated three overlapping admin grants, or that a contractor’s account still holds Salesforce access two months after offboarding
  • Shadow admins hold administrative capability through group nesting, delegated permissions, or app-level roles without ever appearing on the Security → Administrators list, so the admin-count review in Section 7.3 misses them entirely
  • Dormant accounts are the preferred takeover target precisely because no legitimate owner will notice the sign-on; Section 1.6 automates suspension inside Okta, but ISPM finds the same problem in connected clouds where no such automation exists
  • MFA coverage gaps rarely present as an obvious policy misconfiguration — they present as a specific local account, a legacy protocol path, or an application still assigned to the Default Policy, and finding those requires graph analysis rather than a settings check
  • Posture findings are continuous and scored, which converts identity hygiene into a trackable metric for leadership and gives auditors evidence of ongoing monitoring instead of a point-in-time review

Attack Prevented: Privilege escalation via shadow admin paths, dormant account takeover, MFA bypass through uncovered access points, lateral movement using stale offboarded access, privilege creep exploitation

Prerequisites

  • ISPM entitlement confirmed with your Okta account team
  • Super Admin access
  • Read-only service credentials for any connected platform (Entra ID, AWS, Salesforce)

ClickOps Implementation

Step 1: Enable ISPM and Run the First Scan

  1. Confirm ISPM entitlement for your Okta edition with your account team
  2. In the Admin Console, open Identity Security Posture Management from the security navigation
  3. Complete the initial tenant scan and wait for the first posture report to populate

Step 2: Connect Additional Identity Sources

  1. Open the ISPM integrations configuration
  2. Connect the platforms that hold privileged access outside Okta: Microsoft Entra ID / Microsoft 365, AWS, and Salesforce
  3. Grant each connector read-only permissions — posture assessment never requires write access
  4. Re-run the scan and confirm each connected source is reporting identities and permissions

Step 3: Triage the First Posture Report Work the findings in this order:

Priority Finding Type Action
1 Shadow admin accounts and permissions Remove the implicit path or convert it to an explicit, scoped custom admin role (Section 1.2)
2 Offboarded users with active access Deprovision immediately, then fix the offboarding gap that allowed it
3 Access points without MFA Bring under an explicit authentication policy (Sections 1.1 and 1.9)
4 Dormant identities (90+ days) Suspend per Section 1.6 and extend the same rule to connected platforms
5 Over-privileged access and permission creep Reduce to current-role need at the next access review (Section 7.3)

Step 4: Assign Ownership and Route Findings

  1. Assign a named owner for each finding category
  2. Route new critical findings into your ticketing system so they are tracked to closure rather than admired in a dashboard
  3. Forward ISPM events to your SIEM alongside System Log data (Section 5.1)

Step 5: Baseline and Track

  1. Record the initial posture score and per-category finding counts as your baseline
  2. Set a target reduction for each category and review progress monthly
  3. Report the trend to security leadership quarterly alongside the HealthInsight score

Time to Complete: ~2 hours for enablement and connector setup, plus remediation time proportional to findings

Automation: ClickOps only — Okta exposes no write interface for this setting in its Management API, which documents no ISPM endpoint; ISPM connectors and triage are configured in the ISPM console (Okta Management API, spec 2026.08.4, checked 2026-09-24).

Validation & Testing

  1. ISPM is enabled and has completed a full scan of the Okta tenant
  2. All connected identity platforms report identities and permissions successfully
  3. Baseline posture score and per-category finding counts are documented
  4. Every shadow admin finding is either remediated or carries a documented, time-bounded exception
  5. Create a test user, grant it an indirect admin path via nested group membership, and confirm ISPM reports it as a shadow admin
  6. Confirm the “offboarded users with active access” category shows zero findings after remediation

Expected result: Continuous, scored visibility into identity posture across Okta and connected platforms, with shadow admin and stale-access findings driven to zero and tracked over time.

Monitoring & Maintenance

Maintenance schedule:

  • Weekly: Review new critical and high findings
  • Monthly: Full triage pass and posture score trend update
  • Quarterly: Report the posture trend to leadership alongside HealthInsight, and reconcile ISPM findings against the access review in Section 7.3
  • On new platform onboarding: Connect the platform to ISPM as part of the deployment checklist

Note: ISPM complements rather than replaces HealthInsight (Section 5.6). HealthInsight validates tenant configuration against Okta’s built-in task list; ISPM analyzes the identity graph for exploitable access paths across Okta and connected platforms. Run both.

Compliance Mappings

Framework Control Requirement
CIS Controls v8 5.3 Disable dormant accounts
CIS Controls v8 6.2 Establish an access revoking process
NIST 800-53 CA-7 Continuous monitoring of security posture
NIST 800-53 RA-5 Vulnerability monitoring across identity infrastructure
NIST 800-53 AC-2(3) Disable accounts that are dormant or no longer required
NIST 800-53 AC-6(7) Review of user privileges to validate least privilege
SOC 2 CC6.1 Logical access reviewed and restricted to authorized users
SOC 2 CC7.1 Detection of configuration and access deviations

6. Third-Party Integration Security

6.1 Integration Risk Assessment Matrix

Risk Factor Low Medium High
OAuth Scopes Profile read-only Read user data Write users, groups, apps
SCIM Access No SCIM Read-only sync Create/delete users
Admin API No API access Limited endpoints Full API access
Data Access User profile only Group membership Authentication data
Code Pack: API Script
hth-okta-6.01-integration-risk-assessment.sh View source on GitHub ↗
info "6.1 Fetching active applications..."
ACTIVE_APPS=$(okta_get "/api/v1/apps?filter=status%20eq%20%22ACTIVE%22&limit=200")  # a failed read stops the pack
TOTAL_APPS=$(echo "${ACTIVE_APPS}" | jq 'length' 2>/dev/null || echo "0")

Salesforce

Risk Level: High (SSO + Provisioning) Controls:

  • ✅ SCIM token rotation quarterly
  • ✅ Limit provisioned attributes
  • ✅ Enable Salesforce IP restrictions

Microsoft 365

Risk Level: High (Federation) Controls:

  • ✅ Configure federation trust validation
  • ✅ Disable legacy authentication
  • ✅ Sync conditional access policies

GitHub Enterprise

Risk Level: High (Code access) Controls:

  • ✅ SAML SSO with MFA
  • ✅ Disable username/password fallback
  • ✅ Sync team membership carefully
Code Pack: API Script
hth-okta-6.02-common-integrations-controls.sh View source on GitHub ↗
info "6.2 Fetching active OAuth/OIDC applications..."
ACTIVE_APPS=$(okta_get "/api/v1/apps?filter=status%20eq%20%22ACTIVE%22&limit=200")  # a failed read stops the pack
OAUTH_APPS=$(echo "${ACTIVE_APPS}" | jq '[.[] | select(.signOnMode == "OPENID_CONNECT" or .signOnMode == "OAUTH_2_0")]' 2>/dev/null || echo "[]")
OAUTH_COUNT=$(echo "${OAUTH_APPS}" | jq 'length' 2>/dev/null || echo "0")
# Fetch grants for this application
GRANTS=$(okta_get "/api/v1/apps/${APP_ID}/grants")
GRANT_COUNT=$(echo "${GRANTS}" | jq 'length' 2>/dev/null || echo "0")

if [ "${GRANT_COUNT}" -eq 0 ]; then
  info "6.2   ${APP_LABEL}: no explicit scope grants"
  continue
fi

# Extract all granted scope IDs
SCOPE_LIST=$(echo "${GRANTS}" | jq -r '.[].scopeId // empty' 2>/dev/null || true)
# Check default authorization server
AUTH_CLIENTS=$(okta_get "/api/v1/authorizationServers/default/clients")
DEFAULT_CLIENT_COUNT=$(echo "${AUTH_CLIENTS}" | jq 'length' 2>/dev/null || echo "0")

if [ "${DEFAULT_CLIENT_COUNT}" -gt 0 ]; then
  info "6.2 Default auth server has ${DEFAULT_CLIENT_COUNT} registered client(s):"
  echo "${AUTH_CLIENTS}" | jq -r \
    '.[] | "  - \(.client_name // "unnamed") (ID: \(.client_id))"' \
    2>/dev/null || true
else
  info "6.2 No clients registered on default authorization server"
fi
# List all custom authorization servers
AUTH_SERVERS=$(okta_get "/api/v1/authorizationServers")
CUSTOM_SERVERS=$(echo "${AUTH_SERVERS}" | jq '[.[] | select(.name != "default")]' 2>/dev/null || echo "[]")
CUSTOM_COUNT=$(echo "${CUSTOM_SERVERS}" | jq 'length' 2>/dev/null || echo "0")

if [ "${CUSTOM_COUNT}" -gt 0 ]; then
  info "6.2 Found ${CUSTOM_COUNT} custom authorization server(s):"
  echo "${CUSTOM_SERVERS}" | jq -r \
    '.[] | "  - \(.name) (ID: \(.id), audiences: \(.audiences // [] | join(", ")))"' \
    2>/dev/null || true
fi

7. Operational Security

These controls address operational procedures and organizational practices that complement technical hardening. Many are driven by breach post-mortems and SOC 2 audit findings.

7.1 Sanitize HAR Files Before Sharing

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 SC-28, SI-12

Description

Establish a mandatory procedure to sanitize HTTP Archive (HAR) files before sharing with Okta support or any third party. HAR files capture all HTTP traffic including session cookies, authorization tokens, and CSRF tokens. The October 2023 Okta breach was caused by unsanitized HAR files uploaded to Okta’s support system.

Rationale

Why This Matters:

  • HAR files contain active session tokens that can be replayed to hijack user sessions
  • The October 2023 breach affected 134 customers whose HAR files contained valid session cookies
  • Okta support regularly requests HAR files for troubleshooting — this is a recurring operational risk
  • Automated sanitization reduces human error in the manual stripping process

Attack Prevented: Session hijacking via HAR file token exfiltration

Real-World Incidents:

  • October 2023: Threat actor accessed Okta support system and extracted session tokens from HAR files uploaded by 134 customers

ClickOps Implementation

This control has no vendor-console step; it is a procedure your engineers follow on their own machines.

Step 1: Create Organizational Policy Document a formal policy requiring:

  • All HAR files MUST be sanitized before sharing with any external party
  • Engineers must use the automated sanitization script (below) or approved tooling
  • Random audits of support ticket attachments to verify compliance

Step 2: Manual Sanitization Procedure

  1. Open the HAR file in a text editor
  2. Search for and remove all values in these fields:
    • Cookie request headers
    • Authorization request headers
    • Set-Cookie response headers
    • X-Okta-XsrfToken, x-csrf-token, or similar CSRF headers
    • Any Bearer token values
    • sessionToken, stateToken, access_token, id_token, and code values in URLs, query strings, Location headers, and request/response bodies
    • Passwords in request bodies
  3. Save the sanitized file
  4. Verify no sensitive tokens remain by searching for common patterns: sid=, sessionToken, Bearer, SSWS, access_token

Code Implementation

Step 3: Automated Sanitization Script

Code Pack: Config
hth-okta-7.01-sanitize-har-files.sh View source on GitHub ↗
# har-sanitize.sh - Redact credentials and session material from a HAR file
# Usage: ./har-sanitize.sh input.har > sanitized.har
# Redacts: Cookie, Authorization, Proxy-Authorization, X-Okta-XsrfToken,
# X-CSRF-Token and X-Okta-Session request headers; Set-Cookie; all cookie
# values; token-bearing query parameters (URL, queryString, Location header);
# and token/password fields inside request and response bodies (JSON and
# form-encoded). Fails closed: if a known secret pattern survives, it prints
# nothing and exits 1.
set -euo pipefail

INPUT_FILE="${1:-}"
if [ -z "${INPUT_FILE}" ]; then
  echo "Usage: $0 <input.har>" >&2
  exit 1
fi

SANITIZED=$(jq '
  def keys_re: "sessionToken|stateToken|token|code|access_token|id_token|refresh_token|client_secret|password|passcode|answer|state|nonce";
  # key=value pairs in URLs and form bodies
  def redact_pairs: gsub("(?<k>(^|[?&;])(" + keys_re + ")=)[^&#;]*"; "\(.k)[REDACTED]");
  # "key": "value" pairs in JSON bodies
  def redact_json: gsub("(?<k>\"(" + keys_re + ")\"\\s*:\\s*)\"[^\"]*\""; "\(.k)\"[REDACTED]\"");
  def redact_text: if type == "string" then (redact_json | redact_pairs) else . end;
  .log.entries |= map(
    .request.url |= redact_text
    | .request.headers |= map(
        if (.name | test("^(cookie|authorization|proxy-authorization|x-okta-xsrftoken|x-csrf-token|x-okta-session)$"; "i"))
        then .value = "[REDACTED]" else . end)
    | .request.cookies |= map(.value = "[REDACTED]")
    | .request.queryString |= map(
        if (.name | test("^(" + keys_re + ")$"; "i")) then .value = "[REDACTED]" else . end)
    | if .request.postData then
        .request.postData.text |= redact_text
        | .request.postData.params |= (if . then map(.value = "[REDACTED]") else . end)
      else . end
    | .response.headers |= map(
        if (.name | test("^set-cookie$"; "i")) then .value = "[REDACTED]"
        elif (.name | test("^location$"; "i")) then .value |= redact_text
        else . end)
    | .response.cookies |= map(.value = "[REDACTED]")
    | .response.content.text |= redact_text
  )' "${INPUT_FILE}")

# Fail closed: never emit a file that still carries a known secret marker
if printf '%s' "${SANITIZED}" | grep -Eq 'sid=[A-Za-z0-9]|"(sessionToken|stateToken|access_token|id_token|refresh_token|password)" *: *"[^[]|Bearer [A-Za-z0-9._~+/-]{16,}|SSWS [A-Za-z0-9._-]{16,}'; then
  echo "ERROR: secret material survived sanitization -- nothing written" >&2
  exit 1
fi

printf '%s\n' "${SANITIZED}"

Step 4: Alternative Tools

  • Google HAR Sanitizer Chrome Extension — browser-based sanitization
  • BurpSuite — export filtered HAR with token stripping
  • mitmproxy — can export sanitized HAR during capture

Validation & Testing

  1. Sanitization script is available and tested
  2. Policy documented and communicated to all IT/engineering staff
  3. Test: Generate a HAR file, sanitize it, verify no tokens remain by searching for sid=, Bearer, SSWS

7.2 Monitor Okta Security Advisories

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 SI-5, RA-5

Description

Establish a process to monitor Okta security advisories and ensure all Okta client software (Verify, Browser Plugin) is kept up to date. Recent vulnerabilities include DLL hijacking in Okta Verify, XSS in the Browser Plugin, and iOS push notification bypasses.

Rationale

Why This Matters:

  • Okta Verify for Windows was vulnerable to privilege escalation via DLL hijacking, fixed in 5.0.2 (CVE-2024-7061)
  • Okta Browser Plugin versions 6.5.0-6.31.0 were vulnerable to cross-site scripting (CVE-2024-0981)
  • Okta Verify for iOS let an authentication proceed regardless of the user’s selection in its ContextExtension (CVE-2024-10327)
  • Downstream dependencies (React/Next.js CVEs) affect Okta-integrated applications
  • Okta maintains an active bug bounty program (153 valid issues, $405K paid)

Attack Prevented: Exploitation of known vulnerabilities in outdated Okta client software (DLL hijacking privilege escalation, XSS, push bypass)

ClickOps Implementation

Step 1: Subscribe to Security Advisories

  1. Bookmark: trust.okta.com/security-advisories
  2. Subscribe to the page’s Subscribe to RSS Feed link (security-advisories.xml) in your team’s feed reader or alerting channel
  3. Have a Super Admin set the org’s Primary Security Contact in the Okta Help Center (how to) so Okta can notify your security team directly
  4. Add the advisories page to your security team’s weekly monitoring checklist

Step 2: Establish Client Update Policy

  1. Define maximum patch delay: Critical = 48 hours, High = 7 days, Medium = 30 days
  2. Use MDM to enforce Okta client updates:
    • Jamf Pro (macOS): Auto-update Okta Verify via patch management
    • Microsoft Intune (Windows): Deploy Okta Verify updates via Win32 app
    • Chrome Enterprise: Force-update Okta Browser Plugin via policy
  3. Block outdated client versions from authenticating (via Device Assurance policies)

Step 3: Monitor Downstream Dependencies

  1. Track CVEs in frameworks used with Okta authentication:
    • React Server Components (CVE-2025-55182)
    • Next.js middleware (CVE-2025-29927)
    • Auth0 SDK versions
  2. Include Okta dependency monitoring in your vulnerability management program

Code Implementation

The script reads the advisory RSS feed and lists advisories published in the last N days; schedule it (cron or CI) and route its output to your security channel.

Code Pack: API Script
hth-okta-7.02-monitor-okta-security-advisories.sh View source on GitHub ↗
# --fail turns an HTTP error (404, 5xx) into a non-zero exit instead of a body
FEED_XML=$(curl -sS --fail --max-time 30 -A "hth-okta-advisory-monitor" "${FEED}") \
  || die "could not fetch ${FEED}"

COUNT=$(printf '%s' "${FEED_XML}" | xmllint --xpath 'count(/rss/channel/item)' - 2> /dev/null) \
  || die "${FEED} is not well-formed XML"
[[ "${COUNT}" =~ ^[0-9]+$ ]] && [ "${COUNT}" -gt 0 ] \
  || die "feed parsed but held no advisories -- not reporting 'nothing new'"

# One tab-separated line per advisory: pubDate, title, link
# (normalize-space folds any tab or newline inside a field into a space)
TAB=$'\t'
ROWS=$(for i in $(seq 1 "${COUNT}"); do
  printf '%s' "${FEED_XML}" | xmllint --xpath "concat(
    normalize-space(/rss/channel/item[${i}]/pubDate), '${TAB}',
    normalize-space(/rss/channel/item[${i}]/title), '${TAB}',
    normalize-space(/rss/channel/item[${i}]/link))" - || exit 1
  printf '\n'
done) || die "could not read the items in ${FEED}"

# jq stops with an error (and this script with exit 1) on a pubDate it cannot parse
REPORT=$(printf '%s\n' "${ROWS}" | jq -R -s -r --argjson days "${DAYS}" '
  [split("\n")[] | select(length > 0) | split("\t")
   | {date: .[0], title: .[1], link: .[2],
      epoch: (.[0] | sub(" (GMT|UTC|[+]0000)$"; "")
                   | strptime("%a, %d %b %Y %H:%M:%S") | mktime)}] as $items
  | (now - $days * 86400) as $cutoff
  | [$items[] | select(.epoch >= $cutoff)] | sort_by(-.epoch) as $recent
  | "[INFO] 7.2 \($items | length) advisories in the feed; \($recent | length) published in the last \($days) days",
    ($recent[] | "  - \(.epoch | strftime("%Y-%m-%d")) \(.title)\n    \(.link)")') \
  || die "an advisory in ${FEED} has a pubDate that could not be read"

printf '%s\n' "${REPORT}"

Validation & Testing

  1. Security advisory monitoring is assigned to a specific team member
  2. Client update policy is documented and enforced via MDM
  3. Verify all Okta Verify installations are on the latest version

7.3 Conduct Regular Access Reviews

Profile Level: L1 (Crawl)

Framework Control
NIST 800-53 AC-2(3)
SOC 2 CC6.1, CC6.2

Description

Perform periodic access reviews (recertification campaigns) to verify user access is appropriate and remove orphaned accounts, stale privileges, and excessive permissions. SOC 2 auditors specifically look for documented evidence of regular access reviews.

Rationale

Why This Matters:

  • Access accumulates over time as users change roles, so without recertification, entitlements drift far beyond what each person actually needs
  • Orphaned accounts from departed employees and contractors retain valid SSO access to every connected application until someone removes them
  • Excess Super Admin and privileged-group membership multiplies the blast radius of any single account compromise
  • The review itself is the control that catches deprovisioning gaps, and SOC 2 requires documented evidence that it happens regularly

Attack Prevented: Privilege creep, orphaned-account abuse, insider misuse, excessive standing access

ClickOps Implementation

Step 1: Review Admin Accounts

  1. Navigate to: Security → Administrators
  2. Review all admin accounts:
    • Verify each admin is a current employee with legitimate need
    • Count Super Admin accounts — should be fewer than 5
    • Remove admin access for anyone who has changed roles
  3. Document review with date and reviewer name

Step 2: Review User Accounts

  1. Navigate to: Directory → People
  2. Filter by Status: Active
  3. Cross-reference with HR system for terminated employees
  4. Suspend any accounts for users no longer with the organization

Step 3: Review Application Assignments

  1. Navigate to: Applications and Resources → Applications
  2. For each sensitive application, review assigned users/groups
  3. Remove users who no longer need access

Step 4: Review Group Memberships

  1. Navigate to: Directory → Groups
  2. Review privileged groups (admin groups, security groups)
  3. Remove members who no longer need membership

Code Implementation

Code Pack: API Script
hth-okta-7.03-access-reviews.sh View source on GitHub ↗
info "7.3 Finding inactive users (no login in 90+ days)..."
ACTIVE_USERS=$(okta_get "/api/v1/users?filter=status+eq+%22ACTIVE%22&limit=200")  # a failed read stops the pack
TOTAL_ACTIVE=$(printf '%s' "${ACTIVE_USERS}" | jq 'length')
info "7.3 Listing users with admin role assignments..."
ADMIN_USER_IDS=$(okta_get "/api/v1/iam/assignees/users?limit=200" | jq -r '.value[].id')
ADMIN_COUNT=0
SUPER_ADMIN_COUNT=0
for USER_ID in ${ADMIN_USER_IDS}; do
  ADMIN_COUNT=$((ADMIN_COUNT + 1))
  ROLE_TYPES=$(okta_get "/api/v1/users/${USER_ID}/roles" | jq -r '[.[].type] | join(",")')
  info "7.3   Admin user ${USER_ID}: ${ROLE_TYPES}"
  case ",${ROLE_TYPES}," in
    *,SUPER_ADMIN,*) SUPER_ADMIN_COUNT=$((SUPER_ADMIN_COUNT + 1)) ;;
  esac
done

if [ "${SUPER_ADMIN_COUNT}" -ge 5 ]; then
  warn "7.3 Super Admin count: ${SUPER_ADMIN_COUNT} of ${ADMIN_COUNT} admin(s) (should be fewer than 5)"
else
  pass "7.3 Super Admin count: ${SUPER_ADMIN_COUNT} of ${ADMIN_COUNT} admin(s) (within the fewer-than-5 limit)"
fi

Quarterly Access Review Checklist

  • All admin accounts verified against current employee list
  • Super Admin count is < 5
  • No orphaned accounts (users who left but weren’t deprovisioned)
  • No accounts with last login > 90 days (unless exempted)
  • Privileged group memberships reviewed and justified
  • Sensitive application assignments reviewed
  • Review documented with date, reviewer, and findings

Monitoring & Maintenance

Maintenance schedule:

  • Monthly: Review admin accounts for changes
  • Quarterly: Full access review (all users, groups, applications)
  • On employee termination: Immediate account deprovisioning (verify within 24 hours)
  • Annually: Document access review program for SOC 2 auditors

7.4 Implement Change Management for Okta Configuration

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 CM-3
SOC 2 CC8.1

Description

Establish a change management process for Okta configuration changes. All modifications to authentication policies, admin roles, network zones, and integrations should be tracked, approved, and auditable.

Rationale

Why This Matters:

  • Unreviewed changes to authentication policies, admin roles, or IdP configuration can silently weaken or disable security controls across the entire tenant
  • An attacker with admin access can quietly relax MFA, add a malicious IdP, or widen network zones — change tracking makes these modifications visible and reversible
  • Managing configuration as code with peer-reviewed pull requests enforces a second set of eyes before high-impact changes take effect
  • Separation of duties prevents any single admin from both proposing and approving a critical change, satisfying SOC 2 CC8.1 change-control requirements

Attack Prevented: Unauthorized policy weakening, malicious configuration changes, insider sabotage, undetected control drift

ClickOps Implementation

This control is a process; it has no single vendor-console setting.

Step 1: Define Change Categories

Change Type Approval Required Examples
Critical Security team + management Authentication policy changes, admin role modifications, IdP configuration
Standard Security team Application integration, group membership changes, network zone updates
Low Risk Self-approved (with logging) User profile updates, non-privileged group changes

Step 2: Track Configuration as Code

  1. Manage Okta configuration with the okta/okta Terraform provider (this guide’s Terraform packs are a starting point); bring existing objects under management with terraform import
  2. Keep the .tf files in version control, and the Terraform state in an encrypted remote backend with locking — never in git, because state holds secrets such as API tokens and client secrets
  3. Require pull request review for all Okta Terraform changes
  4. Use terraform plan diff as the change documentation

Step 3: Monitor Configuration Changes via System Log Key events to track:

Event Type Description
policy.lifecycle.create New policy created
policy.lifecycle.update Policy modified
policy.lifecycle.delete Policy deleted
policy.rule.create Policy rule created
policy.rule.update Policy rule modified
application.lifecycle.create New application added
application.lifecycle.update Application modified
group.user_membership.add User added to group
group.user_membership.remove User removed from group
zone.lifecycle.create Network zone created
zone.lifecycle.update Network zone modified
system.role.create Admin role created

Step 4: Implement Separation of Duties

  • No single admin can both propose and approve critical changes
  • Require two-person integrity for authentication policy modifications
  • Use Okta Workflows to enforce approval gates for critical changes

Code Implementation

The Sigma rules below alert on the configuration-change events listed in Step 3.

Code Pack: Sigma Detection Rules (5)
hth-okta-7.04-implement-change-management-b.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'application.lifecycle.create'
            - 'application.lifecycle.update'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - eventType
    - target.displayName
    - client.ipAddress
    - published

hth-okta-7.04-implement-change-management-c.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'group.user_membership.add'
            - 'group.user_membership.remove'
    condition: selection
fields:
    - actor.displayName
    - eventType
    - target.displayName
    - client.ipAddress
    - published

hth-okta-7.04-implement-change-management-d.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'zone.lifecycle.create'
            - 'zone.lifecycle.update'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - eventType
    - target.displayName
    - client.ipAddress
    - published

hth-okta-7.04-implement-change-management-e.yml View source on GitHub ↗
detection:
    selection:
        eventType:
            - 'policy.lifecycle.create'
            - 'policy.lifecycle.update'
            - 'policy.lifecycle.delete'
            - 'policy.rule.create'
            - 'policy.rule.update'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - eventType
    - target.displayName
    - client.ipAddress
    - published

hth-okta-7.04-implement-change-management.yml View source on GitHub ↗
detection:
    selection:
        eventType: 'system.role.create'
    condition: selection
fields:
    - actor.displayName
    - actor.alternateId
    - eventType
    - target.displayName
    - client.ipAddress
    - published

Validation & Testing

  1. Change management process documented
  2. Configuration tracked in version control (Terraform or equivalent)
  3. SIEM alerts configured for unauthorized configuration changes
  4. Separation of duties enforced for critical changes

7.5 Establish Identity Incident Response Procedures

Profile Level: L2 (Walk)

Framework Control
NIST 800-53 IR-4, IR-6
SOC 2 CC7.3

Description

Document specific response procedures for identity-based security incidents. These runbooks complement your organization’s broader incident response plan with Okta-specific actions and API calls.

Rationale

Why This Matters:

  • Identity incidents move fast — a compromised admin or stolen session can be used to enroll factors, create IdPs, and pivot before responders understand what happened
  • Pre-written, Okta-specific runbooks let responders suspend accounts, revoke sessions, and deactivate malicious IdPs in minutes instead of improvising under pressure
  • API commands tested in a sandbox ensure containment actions actually work during a real incident rather than failing on unfamiliar syntax
  • Documented procedures satisfy NIST IR-4/IR-6 and SOC 2 CC7.3 and shorten the dwell time of an active attacker

Attack Prevented: Prolonged attacker dwell time, incomplete containment, persistence via factors and IdPs, repeat compromise

ClickOps Implementation

The runbooks below are the procedure; the API calls they reference are in the Code Implementation that follows.

Incident Response Runbooks

Runbook 1: Compromised Admin Account

  1. Contain: Immediately suspend the admin account (see api-ir-suspend-admin in Code Pack)
  2. Revoke: Clear all active sessions (see api-ir-revoke-sessions in Code Pack)
  3. Investigate: Audit all changes made by the compromised account (see api-ir-audit-changes in Code Pack)
  4. Remediate: Reset credentials, re-enroll MFA, review all configuration changes
  5. Restore: Reactivate account only after re-verification of identity

Runbook 2: Stolen Session Tokens

  1. Revoke all active sessions for affected users
  2. Identify the source of token theft (HAR files, malware, XSS)
  3. Block the source IPs in network zones
  4. Force re-authentication for all affected users

Runbook 3: Malicious IdP Creation

  1. Deactivate the malicious IdP immediately (see api-ir-deactivate-idp in Code Pack)
  2. Audit all authentications that used the malicious IdP
  3. Revoke sessions for all users who authenticated via the malicious IdP
  4. Investigate which admin created it and whether their account is compromised

Runbook 4: Unauthorized MFA Enrollment

  1. Remove the unauthorized factor (see api-ir-delete-factor in Code Pack)
  2. Investigate how the enrollment occurred (account takeover, social engineering of helpdesk)
  3. Force password reset and MFA re-enrollment under verified identity
  4. Review all account activity since the unauthorized enrollment

Runbook 5: Mass Password Spray Attack

  1. Activate IP blocking for source IPs via ThreatInsight and network zones
  2. Review lockout logs to identify targeted accounts
  3. Communicate to affected users about potential credential exposure
  4. Force password reset for accounts that were targeted
  5. Verify MFA is enforced – password spray is only effective without MFA

Code Implementation

Code Pack: API Script
hth-okta-7.05-identity-incident-response.sh View source on GitHub ↗
curl -X POST "https://${OKTA_DOMAIN}/api/v1/users/${USER_ID}/lifecycle/suspend" \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}"
curl -X DELETE "https://${OKTA_DOMAIN}/api/v1/users/${USER_ID}/sessions" \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}"
curl -s -X GET "https://${OKTA_DOMAIN}/api/v1/logs?filter=actor.id+eq+%22${USER_ID}%22&since=${INCIDENT_START}" \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}" | jq '.[] | {eventType, target, published}'
curl -X POST "https://${OKTA_DOMAIN}/api/v1/idps/${IDP_ID}/lifecycle/deactivate" \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}"
curl -X DELETE "https://${OKTA_DOMAIN}/api/v1/users/${USER_ID}/factors/${FACTOR_ID}" \
  -H "Authorization: SSWS ${OKTA_API_TOKEN}"

Validation & Testing

  1. All 5 runbooks documented and accessible to security team
  2. API commands tested in sandbox environment
  3. Security team trained on runbook execution
  4. Runbooks integrated into broader incident response plan

Monitoring & Maintenance

Maintenance schedule:

  • Quarterly: Review and update runbooks based on new attack techniques
  • After each incident: Conduct post-incident review and update relevant runbook
  • Annually: Conduct tabletop exercise using runbooks

8. Compliance Quick Reference

8.1 SOC 2 Trust Services Criteria

Control ID Okta Control Guide Section
CC6.1 Phishing-resistant MFA 1.1
CC6.1 Access reviews & recertification 7.3
CC6.1 Help desk visual identity verification 1.13
CC6.1 Cross App Access for AI agents 3.5
CC6.2 Admin role separation 1.2
CC6.6 Network zone policies 2.1
CC6.6 Device assurance policies 1.12
CC7.1 Identity Security Posture Management 5.7
CC7.2 System log monitoring 5.1
CC7.3 Identity incident response 7.5
CC8.1 Change management 7.4

8.2 NIST 800-53 Rev 5

Control Okta Control Guide Section
AC-2 NHI governance 3.4
AC-2(3) Account lifecycle 1.6
AC-2(3) Access reviews 7.3
AC-3 Default authentication policy audit 1.9
AC-3 Dynamic network zones 2.3
AC-3 Cross App Access for AI agents 3.5
AC-5 Admin role separation 1.2
AC-6 Cross App Access for AI agents 3.5
AC-6(1) Custom admin roles 1.2
AC-6(7) Identity Security Posture Management 5.7
AC-7 Account lockout 1.5
AC-12 Session timeouts 4.1
AT-2 Help desk social engineering training 1.13
AU-2 System log 5.1
AU-2 Cross App Access audit records 3.5
AU-6 Cross-tenant impersonation monitoring 5.5
CA-7 HealthInsight reviews 5.6
CA-7 Identity Security Posture Management 5.7
CM-3 Change management 7.4
CM-6 Device assurance policies 1.12
CM-7 OAuth app allowlisting 3.3
IA-2(1) MFA enforcement 1.1
IA-2(6) FIDO2 for admins 1.1
IA-2(12) PIV/CAC authentication 1.7
IA-3 Device assurance policies 1.12
IA-4 NHI governance 3.4
IA-4 Cross App Access for AI agents 3.5
IA-5 Cross App Access for AI agents 3.5
IA-5(1) Password policy 1.4
IA-5(1) Self-service recovery hardening 1.10
IA-5(1) Help desk visual identity verification 1.13
IA-11 Self-service recovery hardening 1.10
IA-12 Help desk visual identity verification 1.13
IR-4 Identity incident response 7.5
IR-6 End-user security notifications 1.11
RA-5 Security advisory monitoring 7.2
RA-5 Identity Threat Protection 5.3
RA-5 Identity Security Posture Management 5.7
SC-7 Dynamic network zones 2.3
SC-13 FIPS compliance 1.8
SC-23 Session persistence 4.2
SC-23 Admin session security 4.3
SC-28 HAR file sanitization 7.1
SI-2 Device assurance policies 1.12
SI-4 End-user security notifications 1.11
SI-4 Identity Threat Protection 5.3
SI-4 Behavior detection 5.4
SI-4 Cross-tenant impersonation monitoring 5.5
SI-5 Security advisory monitoring 7.2
SI-12 HAR file sanitization 7.1

8.3 NIST 800-63-4 AAL Mapping

NIST SP 800-63-4 (final July 2025) defines Authentication Assurance Levels. Map Okta configurations to AAL levels:

AAL Level Okta Configuration Acceptable Authenticators Guide Reference
AAL1 Password only Password (NOT recommended) 1.4
AAL2 Password + any MFA TOTP, Push, FIDO2, Syncable Passkeys 1.1
AAL2 (phishing-resistant) Password + FIDO2 WebAuthn, FastPass, Passkeys 1.1, 1.3
AAL3 Hardware-bound authenticator PIV/CAC, FIDO2 hardware key (non-syncable only) 1.7

Key NIST 800-63-4 Changes:

  • AAL2 MUST offer a phishing-resistant MFA option (Section 1.1)
  • Syncable passkeys are now explicitly accepted at AAL2
  • AAL3 requires hardware-bound authenticators (syncable passkeys NOT acceptable)
  • Introduces Digital Identity Risk Management (DIRM) framework for continuous risk evaluation (Section 5.3)

8.4 DISA STIG Okta IDaaS V1R2

STIG ID Severity Control Guide Section
V-273186 Medium Global session idle timeout (15 min) 4.1
V-273187 Medium Admin Console idle timeout (15 min) 4.1
V-273188 Medium Account inactivity auto-disable (35 days) 1.6
V-273189 Medium Account lockout (3 attempts) 1.5
V-273190 Medium Dashboard phishing-resistant auth 1.1
V-273191 Medium Admin Console phishing-resistant auth 1.1
V-273192 Medium DOD warning banner 8.5
V-273193 HIGH Admin Console MFA required 1.1
V-273194 HIGH Dashboard MFA required 1.1
V-273195 Medium Password min length (15 chars) 1.4
V-273196 Medium Uppercase required 1.4
V-273197 Medium Lowercase required 1.4
V-273198 Medium Number required 1.4
V-273199 Medium Special character required 1.4
V-273200 Medium Min password age (24 hours) 1.4
V-273201 Medium Max password age (60 days) 1.4
V-273202 HIGH Centralized audit logging 5.1
V-273203 Medium Global session lifetime (18 hours) 4.1
V-273204 Medium PIV/CAC credential acceptance 1.7
V-273205 Medium FIPS-compliant Okta Verify 1.8
V-273206 Medium Disable persistent session cookies 4.2
V-273207 Medium Approved CA certificates 1.7
V-273208 Medium Common password check 1.4
V-273209 Medium Password history (5 generations) 1.4
V-279689 Medium API tokens restricted to network zones 3.4
V-279690 Medium API tokens created under dedicated user accounts 3.4
V-279691 Medium Global Session Policy allows or denies by IP per access-control policy 2.1
V-279692 Medium Network zones block anonymized proxies 2.3
V-279693 Medium Network zones defined in each application’s authentication policy 2.1

DISA STIG V1R2 (released 05 Jan 2026) adds the five rules V-279689 through V-279693 to the 24 rules of V1R1 (released 22 Apr 2025): two for API-token hygiene and three for network zones (V1R2 detail view).


8.5 Environment-Specific Requirements

DOD Warning Banner (DISA STIG V-273192)

For U.S. Government systems, display the Standard Mandatory DOD Notice and Consent Banner before granting access. Implementation requires customizing the Okta Sign-In Widget—refer to the “Okta DOD Warning Banner Configuration Guide” in the STIG package.

DOD Banner Text (1300 characters)

8.6 Compliance Checklist

Use this checklist to verify controls are implemented for your compliance requirements.

HIGH Priority Controls (DISA STIG)

  • MFA required for Admin Console (V-273193) — Section 1.1
  • MFA required for Dashboard (V-273194) — Section 1.1
  • Audit logs forwarded to SIEM (V-273202) — Section 5.1

Authentication Controls

  • Phishing-resistant authentication enabled (1.1)
  • Admin role separation implemented (1.2)
  • Password policy configured per requirements (1.4)
  • Account lockout configured (1.5)
  • Account inactivity automation active (1.6)
  • Default authentication policy audited — zero apps assigned (1.9)
  • Self-service recovery hardened — SMS/voice/questions disabled (1.10)
  • End-user security notification emails enabled — all four (1.11)
  • Suspicious activity reporting enabled (1.11)
  • Device assurance policy active for every platform in the fleet (1.12)
  • Help desk visual identity verification required for all resets (1.13)
  • PIV/CAC Smart Card configured (if applicable) (1.7)
  • FIPS compliance enabled (if applicable) (1.8)

Network & Integration Controls

  • Network zones configured (2.1)
  • Admin console access restricted by IP (2.2)
  • Anonymizer/Tor blocking active (2.3)
  • OAuth app allowlisting enforced (3.3)
  • Non-human identity governance implemented (3.4)
  • SSWS to OAuth 2.0 migration planned/completed (3.4)
  • AI agents and MCP servers brokered via Cross App Access or scoped OAuth service apps (3.5)

Session Management

  • Global session idle timeout configured (4.1)
  • Admin Console session timeout configured (4.1)
  • Global session lifetime limited (4.1)
  • Persistent session cookies disabled (4.2)
  • Admin console IP binding enabled (4.3)
  • Protected Actions enabled for critical operations (4.3)

Monitoring & Detection

  • Log streaming or API integration active (5.1)
  • ThreatInsight enabled (5.2)
  • Identity Threat Protection configured (5.3) — if licensed
  • Behavior detection rules active (5.4)
  • Cross-tenant impersonation monitoring alerts configured (5.5)
  • HealthInsight reviewed — all 18 tasks complete or dismissed with a documented reason (5.6)
  • Identity Security Posture Management deployed and findings triaged (5.7)

Operational Security

  • HAR file sanitization procedure documented (7.1)
  • Security advisory monitoring assigned (7.2)
  • Quarterly access reviews scheduled (7.3)
  • Change management process for Okta config (7.4)
  • Identity incident response procedures documented (7.5)

Appendix A: Edition Compatibility

Control Okta Starter Okta SSO Okta Adaptive Okta Identity
MFA ✅ ✅ ✅ ✅
FIDO2/WebAuthn ✅ ✅ ✅ ✅
ThreatInsight ❌ ❌ ✅ ✅
Device Trust ❌ ❌ ✅ ✅
FastPass ❌ ❌ ✅ ✅
Custom Admin Roles ✅ ✅ ✅ ✅
Log Streaming Add-on Add-on ✅ ✅
Workflows/Automations Add-on Add-on ✅ ✅
Identity Threat Protection ❌ ❌ ❌ Add-on
Behavior Detection ❌ ❌ ✅ ✅
HealthInsight ✅ ✅ ✅ ✅
Protected Actions ✅ ✅ ✅ ✅
Enhanced Dynamic Zones ❌ ❌ ✅ ✅
Identity Governance (OIG) ❌ ❌ ❌ Add-on

Editions and packaging change often; confirm with your Okta account team. On an Okta Integrator Free Plan org checked 2026-09-24, Enhanced Dynamic Zones, Behavior Detection, Log Streaming, Device Assurance, and Automations were all available, while Identity Threat Protection and Identity Security Posture Management were not.


Appendix B: References

Official Okta Documentation:

API Documentation:

Compliance Frameworks:

Third-Party Security Research:

CISA & Government:

Security Incidents:


Changelog

Date Version Maturity Changes Author
2026-09-24 0.5.0 ai-drafted · ai-validated Added ai-validated to this guide’s status set, which now reads ai-drafted + ai-validated. A validate-hth-guide run walked this guidance against a live Okta Integrator Free Plan org (Identity Engine) and corrected it until it matched the console; 24 of 36 controls came back live on their ClickOps surface and carry a mark there. An independent audit of the run removed the 1.13 mark, because that control’s visual-verification steps are a written procedure and training rather than a console setting. No Code surface was run with a real credential, because creating the API token is a protected action that needs step-up MFA, so no Code marks. An AI agent did this; no human practitioner has reviewed or applied the guide, so it claims no ni- status. Fixes: current navigation (Applications and Resources, App sign-in policies, Protected Actions on the Okta Admin Console app), 1.2’s auditor role (now the built-in Read-only Administrator, because a custom role cannot grant System Log access), the four security notification emails, HealthInsight’s 18 tasks, DISA STIG V1R2 (five new rules), and the incident figures with sources. New verified packs for 1.3, 1.7, 1.8, 1.12, 2.2, 3.5 and 7.2; evidenced Automation lines for 1.6, 1.13, 5.6 and 5.7. API packs now fail closed and keep the token out of process arguments; payloads were checked against Okta’s API spec; Terraform validates; the HAR sanitizer redacts tokens in bodies and URLs. Claude Code (Opus 5.5)
2026-08-08 0.4.1 ai-drafted Cheat-sheet cell repair: added missing Attack Prevented line(s) to §1.3, §3.2, §4.1, §5.6, §7.2 (no content-facts changed) Claude Code (Fable 5)
2026-08-03 0.4.0 ai-drafted Guidance-currency refresh. Added 4 new controls: Device Assurance Policies including Okta Verify Advanced Posture Checks (1.12), Help Desk Visual Identity Verification (1.13), Cross App Access for AI agents and MCP servers (3.5), Identity Security Posture Management (5.7). Expanded SOC 2 and NIST 800-53 mappings and the compliance checklist to cover the new controls. Claude Code (Sonnet 5)
2026-06-29 0.3.1 ai-drafted Add cheat-sheet Description and Rationale for all controls Claude Code (Opus 4.8)
2026-02-10 0.3.0 ai-drafted Comprehensive audit against Okta SIC, DISA STIG v1.1, NIST 800-63-4, Obsidian/Nudge/AppOmni research. Added 15 new controls: Default Auth Policy Backstop (1.9), Self-Service Recovery (1.10), End-User Notifications (1.11), Dynamic Zones (2.3), OAuth Allowlisting (3.3), NHI Governance (3.4), Admin Session Security (4.3), ITP (5.3), Behavior Detection (5.4), Cross-Tenant Impersonation (5.5), HealthInsight (5.6), HAR Sanitization (7.1), Security Advisory Monitoring (7.2), Access Reviews (7.3), Change Management (7.4), Incident Response (7.5). Expanded compliance mappings with NIST 800-63-4 AAL mapping. Claude Code (Opus 4.6)
2025-12-26 0.2.0 ai-drafted Integrated DISA STIG Okta IDaaS V1R1 controls into functional sections Claude Code (Opus 4.5)
2025-12-14 0.1.0 ai-drafted Initial Okta hardening guide Claude Code (Opus 4.5)

Questions or Improvements?

Contributing

Found an issue or want to improve this guide?