Zscaler Private Access (ZPA) Skill
Diagnose-first guidance for end users writing Terraform HCL that consumes the zscaler/zpa provider. This skill does not cover provider Go code (Plugin SDK schema, expand/flatten, acceptance tests).
Canonical source of truth for resource/data-source schemas: https://registry.terraform.io/providers/zscaler/zpa/latest/docs.
Response Contract
Every ZPA HCL response must include:
- Assumptions & version floor —
zscaler/zpa provider version (~> 4.0 minimum for OneAPI), Terraform/OpenTofu version, auth mode (ASK if not stated — provider supports both OneAPI and legacy v3 as first-class options), ZPA customer ID, cloud target (only set if non-default), microtenant scope (yes/no + which microtenant_id). State assumptions explicitly if the user did not provide them.
- Risk category addressed — one or more of: auth misconfiguration, resource catalog mismatch, policy operand misuse, dependency / detach order, microtenant scoping, drift, secret exposure.
- Chosen approach & tradeoffs — what was chosen, what was traded off (e.g. data-source lookup vs hardcoded ID), why.
- Validation plan — exact commands:
terraform fmt -check, terraform validate, terraform plan -out=tfplan, optional terraform show -json tfplan | jq to inspect operand JSON before apply.
- Rollback notes — for any policy-rule or segment-group change: how to undo (re-apply previous HCL,
terraform state rm not safe for ZPA — see Troubleshooting), what evidence to keep (debug log capture).
Never recommend terraform apply against a production ZPA tenant without a reviewed plan artifact and a microtenant scope check.
Workflow
- Capture context (see fields below).
- Diagnose intent using the routing table.
- Load only the matching reference file(s). Do not preload depth the task does not need.
- Propose HCL grounded in the canonical examples from
references/resource-catalog.md — never invent attribute names.
- Validate with the commands tailored to risk tier.
- Emit the Response Contract.
Capture Context — Fields to Confirm
| Field |
Why it matters |
Default if missing |
| Provider version |
Resource catalog and auth options differ between v3 (legacy) and v4+ (OneAPI). Always pin ~> 4.0 minimum. |
Assume ~> 4.0 and state it. |
| Auth mode |
OneAPI and legacy v3 are both first-class. Tenant must be migrated to Zidentity for OneAPI; otherwise legacy is the only option. FedRAMP clouds work on either path — OneAPI from v4.4.6. |
Ask. Do not default. State both options if unclear. |
| Cloud target |
OneAPI: zscaler_cloud is optional for commercial production; set gov / govus for FedRAMP, beta for non-prod. Legacy: zpa_cloud is required only when not PRODUCTION (BETA, ZPATWO, GOV, GOVUS, PREVIEW). |
Omit zscaler_cloud for commercial production OneAPI. Omit zpa_cloud for legacy PRODUCTION. |
| Customer ID |
Required for both auth modes — tenant-scoped (ZPA_CUSTOMER_ID). |
Ask if absent. |
| Microtenant |
Many resources are microtenant-scoped. Mixing scopes silently breaks Read. |
Assume parent tenant; flag risk. |
| Terraform runtime |
Affects optional(), moved, import, write_only availability. |
Assume terraform ~> 1.9. |
Diagnose Before You Generate
| Failure category |
Symptoms |
Primary references |
| Auth misconfiguration |
401 unauthorized, vanity_domain not found, Cloud GOV not supported for OneAPI |
Auth & Providers |
| Resource catalog mismatch |
"Does ZPA have a resource for X?", invented attribute names, wrong block structure |
Resource Catalog |
| Policy operand misuse |
400 INVALID_INPUT on policy rule, Invalid operand type, LHS value is required |
Policy Rules: Operand Reference |
| Policy ordering / type |
Wrong policy_set_id, rule applied in wrong policy, action enum rejected |
Policy Rules: Policy Type Map |
| Dependency / detach order |
RESOURCE_IN_USE on terraform destroy of segment / server / app-connector group |
Troubleshooting: Detach-Before-Delete |
| Microtenant scoping |
Resource exists in console but Read returns 404 → Terraform recreates |
Troubleshooting: Microtenant 404 |
| Drift on every plan |
Bool attribute keeps flipping, set order changes, write-only field clears |
Troubleshooting: Drift Causes |
| Secret exposure |
Client secrets / private keys in .tfvars, in state, in CI logs |
Auth & Providers: Credential Hygiene |
| Cross-product question |
Prompt mentions ZIA / ZTC / ZCC alongside ZPA, asks "what's the ZIA equivalent of …", or wires ZPA into a ZIA forwarding gateway |
best-practices-skill → Cross-Product Equivalents |
Provider Block — Pick One
The provider supports two auth paths. Pick based on whether the tenant has been migrated to Zidentity. Do not default — confirm with the user.
Authoring rule (do not summarise): when emitting an OneAPI provider block, reproduce the env-var comment list verbatim — including ZSCALER_CLOUD with its optional annotation. Users need to discover that ZSCALER_CLOUD exists as a supported (but optional) parameter for OneAPI; condensing it to # ZSCALER_CLIENT_ID, ZSCALER_CLIENT_SECRET, ZSCALER_VANITY_DOMAIN, ZPA_CUSTOMER_ID hides that fact.
OneAPI (Zidentity tenants)
terraform {
required_version = "~> 1.9"
required_providers {
zpa = {
source = "zscaler/zpa"
version = "~> 4.0"
}
}
}
provider "zpa" {
# In CI, set these env vars instead of hardcoding. The first four are required,
# the fifth is optional and only used to target a non-production Zidentity environment.
# ZSCALER_CLIENT_ID (required)
# ZSCALER_CLIENT_SECRET (required; or ZSCALER_PRIVATE_KEY)
# ZSCALER_VANITY_DOMAIN (required)
# ZPA_CUSTOMER_ID (required)
# ZSCALER_CLOUD (optional — only set for non-prod, e.g. "beta")
}
Legacy v3 (pre-Zidentity tenants, or FedRAMP below v4.4.6)
provider "zpa" {
use_legacy_client = true
# Env vars:
# ZPA_CLIENT_ID, ZPA_CLIENT_SECRET, ZPA_CUSTOMER_ID
# ZPA_CLOUD ← REQUIRED only when not PRODUCTION: BETA | ZPATWO | GOV | GOVUS | PREVIEW
# ZSCALER_USE_LEGACY_CLIENT=true
}
❌ Do not set zscaler_cloud = "PRODUCTION" on OneAPI — PRODUCTION is a legacy zpa_cloud value. On OneAPI, omit zscaler_cloud entirely for production tenants.
For private-key auth and microtenant scoping, see Auth & Providers.
Resource Hierarchy
| Layer |
Purpose |
Example resources |
| Connectivity |
Where ZPA reaches into the network |
zpa_app_connector_group, zpa_app_connector_controller |
| Application |
What is being protected |
zpa_application_server, zpa_application_segment, zpa_application_segment_browser_access |
| Grouping |
How applications and servers are grouped for policy targeting |
zpa_segment_group, zpa_server_group |
| Identity |
Where users come from |
zpa_idp_controller, zpa_scim_groups, zpa_scim_attribute_header, zpa_saml_attribute |
| Posture |
Device-trust signals |
zpa_posture_profile, zpa_trusted_network |
| Policy |
Allow / deny / forward / inspect / isolate |
zpa_policy_access_rule, zpa_policy_access_forwarding_rule, zpa_policy_access_isolation_rule, zpa_policy_access_inspection_rule |
| Tenancy |
Microtenant scoping (optional) |
zpa_microtenant_controller |
Standard composition flow: Application Server → Server Group ← Segment Group ← Application Segment → referenced by Policy Rule. See Resource Catalog: Composition Recipes.
Naming Conventions
- Use descriptive names:
resource "zpa_application_segment" "crm_app", not ... "this".
- Reserve
"this" for genuine singletons (e.g. a single zpa_microtenant_controller).
- Prefix variables with context:
zpa_segment_group_id, not id.
- Standard files:
main.tf, variables.tf, outputs.tf, versions.tf, providers.tf.
Block Ordering
Resource blocks: count/for_each first → required arguments → optional arguments → nested blocks → lifecycle.
Variable blocks: description → type → default → validation → nullable → sensitive.
Policy Rules — Quick Rules
- Always look up
policy_set_id via data "zpa_policy_type" — never hardcode.
- Always prefer data sources (
data.zpa_application_segment.x.id) over literal IDs.
conditions is an ordered list of OR-groups combined by the rule-level operator. Inside each conditions block, multiple operands are combined by that block's operator.
operands.object_type is a closed enum (APP, APP_GROUP, SCIM, SCIM_GROUP, SAML, IDP, POSTURE, TRUSTED_NETWORK, CLIENT_TYPE, PLATFORM, COUNTRY_CODE, MACHINE_GRP, …).
lhs / rhs semantics depend on object_type. For SCIM_GROUP, lhs = idp_id, rhs = scim_group_id. For APP, lhs = "id", rhs = application_segment_id. Get this wrong → 400 INVALID_INPUT.
Full mapping in Policy Rules: Operand Reference.
Microtenants — When to Worry
If the customer's tenant uses microtenants, every resource and every data source call must pass the same microtenant_id. Mixing scopes silently breaks Read (returns 404 → Terraform recreates the resource). See Troubleshooting: Microtenant 404.
resource "zpa_application_segment" "crm" {
name = "CRM"
microtenant_id = var.zpa_microtenant_id # propagate consistently
# ...
}
If the user does not mention microtenants, assume parent tenant and state that assumption in the Response Contract.
Data-Source-Only Objects
ZPA exposes 70 resources and 70 data sources, but ~30 of those data sources have no matching resource because the underlying object is provisioned by another system (IdP, posture vendor, ZPA console, cloud orchestration). Common data-source-only objects:
| Category |
Data sources only (representative — not exhaustive) |
| Identity (IdP-driven) |
zpa_idp_controller, zpa_saml_attribute, zpa_scim_attribute_header (you can use zpa_scim_groups for SCIM groups but the IdP is read-only) |
| Posture (vendor-driven) |
zpa_posture_profile, zpa_machine_group, zpa_trusted_network (often read-only when populated by an MDM) |
| Cloud orchestration |
zpa_app_connector_controller, zpa_private_cloud_controller, zpa_branch_connector_group, zpa_cloud_connector_group, zpa_extranet_resource_partner |
| Tenant / catalog |
zpa_enrollment_cert, zpa_customer_version_profile, zpa_policy_type, zpa_risk_score_values |
| Helper / lookup |
zpa_application_segment_by_type, zpa_access_policy_client_types, zpa_access_policy_platforms, zpa_lss_config_client_types, zpa_lss_config_log_type_formats, zpa_lss_config_status_codes |
| Browser / isolation |
zpa_browser_protection, zpa_managed_browser_profile, zpa_isolation_profile, zpa_cloud_browser_isolation_region, zpa_cloud_browser_isolation_zpa_profile |
| Inspection catalog |
zpa_inspection_predefined_controls, zpa_inspection_all_predefined_controls |
| Location reference |
zpa_location_controller, zpa_location_controller_summary, zpa_location_group_controller |
Always check the official Terraform Registry before assuming there's a matching resource:
- Resources index: https://registry.terraform.io/providers/zscaler/zpa/latest/docs
- Specific resource:
https://registry.terraform.io/providers/zscaler/zpa/latest/docs/resources/<name_without_zpa_prefix>
- Specific data source:
https://registry.terraform.io/providers/zscaler/zpa/latest/docs/data-sources/<name_without_zpa_prefix>
If only the data-source page exists for an object, it's read-only.
❌ Do not propose resource "zpa_idp_controller" — IdPs are configured in the ZPA admin console, not by Terraform. ✅ Use data "zpa_idp_controller" { name = "Okta" } to look one up for use in policy operands.
Credential Hygiene
- ❌ Never put
client_secret, private_key, or customer_id in .tfvars checked into git.
- ❌ Never echo credentials in CI job logs.
- ❌ Do not mix
ZSCALER_* and ZPA_* env vars in the same job — the provider picks one path based on use_legacy_client and silently ignores the other namespace. (ZPA_CUSTOMER_ID is the exception — it is required for both modes.)
- ✅ Use env vars matching your auth path:
- OneAPI:
ZSCALER_CLIENT_ID, ZSCALER_CLIENT_SECRET (or ZSCALER_PRIVATE_KEY), ZSCALER_VANITY_DOMAIN, ZPA_CUSTOMER_ID, optionally ZSCALER_CLOUD for non-prod.
- Legacy:
ZPA_CLIENT_ID, ZPA_CLIENT_SECRET, ZPA_CUSTOMER_ID, ZSCALER_USE_LEGACY_CLIENT=true, plus ZPA_CLOUD for non-PRODUCTION clouds.
- ✅ Source from your secret store (Vault, AWS Secrets Manager, GH Actions secrets) and inject as env at job start.
- ✅ Mark any HCL variable that holds these
sensitive = true even though it's display-only — it prevents accidental terraform output / log leakage.
State files contain identifiers (segment IDs, server IDs) but not the OneAPI client secret, since auth is config-only and not persisted. Still, treat state as sensitive — restrict S3/GCS bucket access.
Reference Files
Progressive disclosure — essentials here, depth on demand:
- Auth & Providers — provider config, OneAPI vs legacy, env vars, GOV cloud, microtenant config, credential hygiene.
- Resource Catalog — minimum-viable HCL per resource, composition recipes, data-source lookups.
- Policy Rules — policy type map, operand reference, condition composition, ordering, common 400 errors.
- Troubleshooting — drift causes, detach-before-delete, microtenant 404, debug logging.
- Recent Provider Changes — auto-mined from the upstream provider CHANGELOG; lists user-facing additions and breaking changes from the last several releases.
Cross-cutting engineering discipline (state organization, microtenant blast radius, CI/CD, secret handling, testing strategy, modules, naming, versioning) lives in the sibling best-practices-skill — load it whenever the question is about how to structure or operate a Zscaler-Terraform repo rather than how to call a specific zpa_* resource.
Authoring Rule — Grounding for Uncatalogued Resources
The reference catalog ships canonical HCL for the most-used zpa_* resources, but ZPA exposes 70 resources and 70 data sources. When asked about an object not in Resource Catalog:
- Fetch the official Registry page first before generating any HCL:
- Resource:
https://registry.terraform.io/providers/zscaler/zpa/latest/docs/resources/<name_without_zpa_prefix>
- Data source:
https://registry.terraform.io/providers/zscaler/zpa/latest/docs/data-sources/<name_without_zpa_prefix>
- Ground every attribute name in that fetched page.
- State the Registry URL you used in the Response Contract
Assumptions section.
❌ Never invent attribute names. ✅ If the Registry page does not exist for a zpa_<name>, the resource does not exist — say so explicitly.
What This Skill Will Not Do
- Generate HCL with attribute names not documented on the official Terraform Registry page for that resource.
- Recommend
terraform state rm for ZPA resources (orphans the API object, see Troubleshooting).
- Cover provider development (Plugin SDK schema, expand/flatten, acceptance tests) — out of scope.
1---2name: zpa-skill3description: Use when writing, reviewing, or debugging Terraform HCL that uses the Zscaler Private Access (ZPA) provider — covers provider auth (OneAPI / legacy / multi-cloud), the resource catalog (application segments, server groups, segment groups, app connector groups, policy access rules), policy-rule operand semantics, and known API quirks (drift on omitempty bools, microtenant scoping, detach-before-delete).4license: MIT5---67# Zscaler Private Access (ZPA) Skill89Diagnose-first guidance for **end users writing Terraform HCL that consumes the `zscaler/zpa` provider**. This skill does not cover provider Go code (Plugin SDK schema, expand/flatten, acceptance tests).1011**Canonical source of truth** for resource/data-source schemas: <https://registry.terraform.io/providers/zscaler/zpa/latest/docs>.1213## Response Contract1415Every ZPA HCL response must include:16171. **Assumptions & version floor** — `zscaler/zpa` provider version (`~> 4.0` minimum for OneAPI), Terraform/OpenTofu version, **auth mode (ASK if not stated — provider supports both OneAPI and legacy v3 as first-class options)**, ZPA customer ID, cloud target (only set if non-default), microtenant scope (yes/no + which `microtenant_id`). State assumptions explicitly if the user did not provide them.182. **Risk category addressed** — one or more of: auth misconfiguration, resource catalog mismatch, policy operand misuse, dependency / detach order, microtenant scoping, drift, secret exposure.193. **Chosen approach & tradeoffs** — what was chosen, what was traded off (e.g. data-source lookup vs hardcoded ID), why.204. **Validation plan** — exact commands: `terraform fmt -check`, `terraform validate`, `terraform plan -out=tfplan`, optional `terraform show -json tfplan | jq` to inspect operand JSON before apply.215. **Rollback notes** — for any policy-rule or segment-group change: how to undo (re-apply previous HCL, `terraform state rm` not safe for ZPA — see [Troubleshooting](references/troubleshooting.md#never-state-rm-a-zpa-resource)), what evidence to keep (debug log capture).2223Never recommend `terraform apply` against a production ZPA tenant without a reviewed plan artifact and a microtenant scope check.2425## Workflow26271. **Capture context** (see fields below).282. **Diagnose intent** using the routing table.293. **Load only the matching reference file(s).** Do not preload depth the task does not need.304. **Propose HCL** grounded in the canonical examples from `references/resource-catalog.md` — never invent attribute names.315. **Validate** with the commands tailored to risk tier.326. **Emit the Response Contract.**3334## Capture Context — Fields to Confirm3536| Field | Why it matters | Default if missing |37| ----------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------- |38| Provider version | Resource catalog and auth options differ between v3 (legacy) and v4+ (OneAPI). Always pin `~> 4.0` minimum. | Assume `~> 4.0` and state it. |39| **Auth mode** | **OneAPI and legacy v3 are both first-class.** Tenant must be migrated to Zidentity for OneAPI; otherwise legacy is the only option. FedRAMP clouds work on **either** path — OneAPI from `v4.4.6`. | **Ask. Do not default.** State both options if unclear. |40| Cloud target | OneAPI: `zscaler_cloud` is **optional** for commercial production; set `gov` / `govus` for FedRAMP, `beta` for non-prod. Legacy: `zpa_cloud` is required only when **not** `PRODUCTION` (`BETA`, `ZPATWO`, `GOV`, `GOVUS`, `PREVIEW`). | **Omit `zscaler_cloud` for commercial production OneAPI. Omit `zpa_cloud` for legacy PRODUCTION.** |41| Customer ID | Required for both auth modes — tenant-scoped (`ZPA_CUSTOMER_ID`). | Ask if absent. |42| Microtenant | Many resources are microtenant-scoped. Mixing scopes silently breaks Read. | Assume parent tenant; flag risk. |43| Terraform runtime | Affects `optional()`, `moved`, `import`, `write_only` availability. | Assume `terraform ~> 1.9`. |4445## Diagnose Before You Generate4647| Failure category | Symptoms | Primary references |48| --------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |49| **Auth misconfiguration** | `401 unauthorized`, `vanity_domain not found`, `Cloud GOV not supported for OneAPI` | [Auth & Providers](references/auth-and-providers.md) |50| **Resource catalog mismatch** | "Does ZPA have a resource for X?", invented attribute names, wrong block structure | [Resource Catalog](references/resource-catalog.md) |51| **Policy operand misuse** | `400 INVALID_INPUT` on policy rule, `Invalid operand type`, `LHS value is required` | [Policy Rules: Operand Reference](references/policy-rules.md#operand-reference) |52| **Policy ordering / type** | Wrong `policy_set_id`, rule applied in wrong policy, action enum rejected | [Policy Rules: Policy Type Map](references/policy-rules.md#policy-type-map) |53| **Dependency / detach order** | `RESOURCE_IN_USE` on `terraform destroy` of segment / server / app-connector group | [Troubleshooting: Detach-Before-Delete](references/troubleshooting.md#detach-before-delete) |54| **Microtenant scoping** | Resource exists in console but Read returns 404 → Terraform recreates | [Troubleshooting: Microtenant 404](references/troubleshooting.md#microtenant-not-found) |55| **Drift on every plan** | Bool attribute keeps flipping, set order changes, write-only field clears | [Troubleshooting: Drift Causes](references/troubleshooting.md#drift-causes) |56| **Secret exposure** | Client secrets / private keys in `.tfvars`, in state, in CI logs | [Auth & Providers: Credential Hygiene](references/auth-and-providers.md#credential-hygiene) |57| **Cross-product question** | Prompt mentions ZIA / ZTC / ZCC alongside ZPA, asks "what's the ZIA equivalent of …", or wires ZPA into a ZIA forwarding gateway | `best-practices-skill` → [Cross-Product Equivalents](../best-practices-skill/references/cross-product-equivalents.md) |5859## Provider Block — Pick One6061The provider supports **two** auth paths. Pick based on whether the tenant has been migrated to Zidentity. **Do not default** — confirm with the user.6263> **Authoring rule (do not summarise):** when emitting an OneAPI provider block, reproduce the env-var comment list **verbatim** — including `ZSCALER_CLOUD` with its `optional` annotation. Users need to discover that `ZSCALER_CLOUD` exists as a supported (but optional) parameter for OneAPI; condensing it to `# ZSCALER_CLIENT_ID, ZSCALER_CLIENT_SECRET, ZSCALER_VANITY_DOMAIN, ZPA_CUSTOMER_ID` hides that fact.6465### OneAPI (Zidentity tenants)6667```hcl68terraform {69 required_version = "~> 1.9"70 required_providers {71 zpa = {72 source = "zscaler/zpa"73 version = "~> 4.0"74 }75 }76}7778provider "zpa" {79 # In CI, set these env vars instead of hardcoding. The first four are required,80 # the fifth is optional and only used to target a non-production Zidentity environment.81 # ZSCALER_CLIENT_ID (required)82 # ZSCALER_CLIENT_SECRET (required; or ZSCALER_PRIVATE_KEY)83 # ZSCALER_VANITY_DOMAIN (required)84 # ZPA_CUSTOMER_ID (required)85 # ZSCALER_CLOUD (optional — only set for non-prod, e.g. "beta")86}87```8889### Legacy v3 (pre-Zidentity tenants, or FedRAMP below `v4.4.6`)9091```hcl92provider "zpa" {93 use_legacy_client = true94 # Env vars:95 # ZPA_CLIENT_ID, ZPA_CLIENT_SECRET, ZPA_CUSTOMER_ID96 # ZPA_CLOUD ← REQUIRED only when not PRODUCTION: BETA | ZPATWO | GOV | GOVUS | PREVIEW97 # ZSCALER_USE_LEGACY_CLIENT=true98}99```100101❌ Do not set `zscaler_cloud = "PRODUCTION"` on OneAPI — `PRODUCTION` is a **legacy** `zpa_cloud` value. On OneAPI, omit `zscaler_cloud` entirely for production tenants.102103For private-key auth and microtenant scoping, see [Auth & Providers](references/auth-and-providers.md).104105## Resource Hierarchy106107| Layer | Purpose | Example resources |108| ---------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |109| **Connectivity** | Where ZPA reaches into the network | `zpa_app_connector_group`, `zpa_app_connector_controller` |110| **Application** | What is being protected | `zpa_application_server`, `zpa_application_segment`, `zpa_application_segment_browser_access` |111| **Grouping** | How applications and servers are grouped for policy targeting | `zpa_segment_group`, `zpa_server_group` |112| **Identity** | Where users come from | `zpa_idp_controller`, `zpa_scim_groups`, `zpa_scim_attribute_header`, `zpa_saml_attribute` |113| **Posture** | Device-trust signals | `zpa_posture_profile`, `zpa_trusted_network` |114| **Policy** | Allow / deny / forward / inspect / isolate | `zpa_policy_access_rule`, `zpa_policy_access_forwarding_rule`, `zpa_policy_access_isolation_rule`, `zpa_policy_access_inspection_rule` |115| **Tenancy** | Microtenant scoping (optional) | `zpa_microtenant_controller` |116117Standard composition flow: **Application Server → Server Group ← Segment Group ← Application Segment → referenced by Policy Rule.** See [Resource Catalog: Composition Recipes](references/resource-catalog.md#composition-recipes).118119## Naming Conventions120121- Use descriptive names: `resource "zpa_application_segment" "crm_app"`, not `... "this"`.122- Reserve `"this"` for genuine singletons (e.g. a single `zpa_microtenant_controller`).123- Prefix variables with context: `zpa_segment_group_id`, not `id`.124- Standard files: `main.tf`, `variables.tf`, `outputs.tf`, `versions.tf`, `providers.tf`.125126## Block Ordering127128Resource blocks: `count`/`for_each` first → required arguments → optional arguments → nested blocks → `lifecycle`.129Variable blocks: `description` → `type` → `default` → `validation` → `nullable` → `sensitive`.130131## Policy Rules — Quick Rules132133- **Always** look up `policy_set_id` via `data "zpa_policy_type"` — never hardcode.134- **Always** prefer data sources (`data.zpa_application_segment.x.id`) over literal IDs.135- `conditions` is an **ordered list of OR-groups** combined by the rule-level `operator`. Inside each `conditions` block, multiple `operands` are combined by that block's `operator`.136- `operands.object_type` is a closed enum (`APP`, `APP_GROUP`, `SCIM`, `SCIM_GROUP`, `SAML`, `IDP`, `POSTURE`, `TRUSTED_NETWORK`, `CLIENT_TYPE`, `PLATFORM`, `COUNTRY_CODE`, `MACHINE_GRP`, …).137- `lhs` / `rhs` semantics depend on `object_type`. For `SCIM_GROUP`, `lhs = idp_id`, `rhs = scim_group_id`. For `APP`, `lhs = "id"`, `rhs = application_segment_id`. Get this wrong → `400 INVALID_INPUT`.138139Full mapping in [Policy Rules: Operand Reference](references/policy-rules.md#operand-reference).140141## Microtenants — When to Worry142143If the customer's tenant uses microtenants, **every** resource and **every** data source call must pass the same `microtenant_id`. Mixing scopes silently breaks Read (returns 404 → Terraform recreates the resource). See [Troubleshooting: Microtenant 404](references/troubleshooting.md#microtenant-not-found).144145```hcl146resource "zpa_application_segment" "crm" {147 name = "CRM"148 microtenant_id = var.zpa_microtenant_id # propagate consistently149 # ...150}151```152153If the user does not mention microtenants, assume parent tenant and **state that assumption** in the Response Contract.154155## Data-Source-Only Objects156157ZPA exposes 70 resources and 70 data sources, but ~30 of those data sources have **no matching resource** because the underlying object is provisioned by another system (IdP, posture vendor, ZPA console, cloud orchestration). Common data-source-only objects:158159| Category | Data sources only (representative — not exhaustive) |160| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |161| Identity (IdP-driven) | `zpa_idp_controller`, `zpa_saml_attribute`, `zpa_scim_attribute_header` (you can use `zpa_scim_groups` for SCIM groups but the IdP is read-only) |162| Posture (vendor-driven) | `zpa_posture_profile`, `zpa_machine_group`, `zpa_trusted_network` (often read-only when populated by an MDM) |163| Cloud orchestration | `zpa_app_connector_controller`, `zpa_private_cloud_controller`, `zpa_branch_connector_group`, `zpa_cloud_connector_group`, `zpa_extranet_resource_partner` |164| Tenant / catalog | `zpa_enrollment_cert`, `zpa_customer_version_profile`, `zpa_policy_type`, `zpa_risk_score_values` |165| Helper / lookup | `zpa_application_segment_by_type`, `zpa_access_policy_client_types`, `zpa_access_policy_platforms`, `zpa_lss_config_client_types`, `zpa_lss_config_log_type_formats`, `zpa_lss_config_status_codes` |166| Browser / isolation | `zpa_browser_protection`, `zpa_managed_browser_profile`, `zpa_isolation_profile`, `zpa_cloud_browser_isolation_region`, `zpa_cloud_browser_isolation_zpa_profile` |167| Inspection catalog | `zpa_inspection_predefined_controls`, `zpa_inspection_all_predefined_controls` |168| Location reference | `zpa_location_controller`, `zpa_location_controller_summary`, `zpa_location_group_controller` |169170Always check the official Terraform Registry before assuming there's a matching resource:171172- Resources index: <https://registry.terraform.io/providers/zscaler/zpa/latest/docs>173- Specific resource: `https://registry.terraform.io/providers/zscaler/zpa/latest/docs/resources/<name_without_zpa_prefix>`174- Specific data source: `https://registry.terraform.io/providers/zscaler/zpa/latest/docs/data-sources/<name_without_zpa_prefix>`175176If only the data-source page exists for an object, it's read-only.177178❌ Do not propose `resource "zpa_idp_controller"` — IdPs are configured in the ZPA admin console, not by Terraform. ✅ Use `data "zpa_idp_controller" { name = "Okta" }` to look one up for use in policy operands.179180## Credential Hygiene181182- ❌ Never put `client_secret`, `private_key`, or `customer_id` in `.tfvars` checked into git.183- ❌ Never echo credentials in CI job logs.184- ❌ **Do not mix `ZSCALER_*` and `ZPA_*` env vars in the same job** — the provider picks one path based on `use_legacy_client` and silently ignores the other namespace. (`ZPA_CUSTOMER_ID` is the exception — it is required for both modes.)185- ✅ Use env vars matching your auth path:186 - **OneAPI:** `ZSCALER_CLIENT_ID`, `ZSCALER_CLIENT_SECRET` (or `ZSCALER_PRIVATE_KEY`), `ZSCALER_VANITY_DOMAIN`, `ZPA_CUSTOMER_ID`, optionally `ZSCALER_CLOUD` for non-prod.187 - **Legacy:** `ZPA_CLIENT_ID`, `ZPA_CLIENT_SECRET`, `ZPA_CUSTOMER_ID`, `ZSCALER_USE_LEGACY_CLIENT=true`, plus `ZPA_CLOUD` for non-PRODUCTION clouds.188- ✅ Source from your secret store (Vault, AWS Secrets Manager, GH Actions secrets) and inject as env at job start.189- ✅ Mark any HCL variable that holds these `sensitive = true` even though it's display-only — it prevents accidental `terraform output` / log leakage.190191State files contain identifiers (segment IDs, server IDs) but **not** the OneAPI client secret, since auth is config-only and not persisted. Still, treat state as sensitive — restrict S3/GCS bucket access.192193## Reference Files194195Progressive disclosure — essentials here, depth on demand:196197- [Auth & Providers](references/auth-and-providers.md) — provider config, OneAPI vs legacy, env vars, GOV cloud, microtenant config, credential hygiene.198- [Resource Catalog](references/resource-catalog.md) — minimum-viable HCL per resource, composition recipes, data-source lookups.199- [Policy Rules](references/policy-rules.md) — policy type map, operand reference, condition composition, ordering, common 400 errors.200- [Troubleshooting](references/troubleshooting.md) — drift causes, detach-before-delete, microtenant 404, debug logging.201- [Recent Provider Changes](references/recent-provider-changes.md) — auto-mined from the upstream provider CHANGELOG; lists user-facing additions and breaking changes from the last several releases.202203**Cross-cutting engineering discipline** (state organization, microtenant blast radius, CI/CD, secret handling, testing strategy, modules, naming, versioning) lives in the sibling **`best-practices-skill`** — load it whenever the question is about how to structure or operate a Zscaler-Terraform repo rather than how to call a specific `zpa_*` resource.204205## Authoring Rule — Grounding for Uncatalogued Resources206207The reference catalog ships canonical HCL for the most-used `zpa_*` resources, but ZPA exposes 70 resources and 70 data sources. When asked about an object not in [Resource Catalog](references/resource-catalog.md):2082091. **Fetch the official Registry page first** before generating any HCL:210 - Resource: `https://registry.terraform.io/providers/zscaler/zpa/latest/docs/resources/<name_without_zpa_prefix>`211 - Data source: `https://registry.terraform.io/providers/zscaler/zpa/latest/docs/data-sources/<name_without_zpa_prefix>`2122. Ground every attribute name in that fetched page.2133. State the Registry URL you used in the Response Contract `Assumptions` section.214215❌ Never invent attribute names. ✅ If the Registry page does not exist for a `zpa_<name>`, the resource does not exist — say so explicitly.216217## What This Skill Will Not Do218219- Generate HCL with attribute names not documented on the official Terraform Registry page for that resource.220- Recommend `terraform state rm` for ZPA resources (orphans the API object, see [Troubleshooting](references/troubleshooting.md#never-state-rm-a-zpa-resource)).221- Cover provider development (Plugin SDK schema, expand/flatten, acceptance tests) — out of scope.