Raxx · internal docs

internal · gated

Terraform cf-access state imports runbook

System: terraform/cf-access Owner: operator Last incident: 2026-05-12 (see #1864) Last reviewed: 2026-05-12

Background

The terraform/cf-access stack manages Cloudflare Zero Trust Access applications and policies, WAF rulesets, and identity providers. Resources that pre-existed Terraform management must be imported into state before terraform apply can be run safely.

This runbook documents the exact import commands and IDs for the 5 resources that were out-of-state as of 2026-05-12 (#1864), including two active blockers that require operator action before imports can complete.


Active blockers before imports can succeed

Blocker 1: No single CF API token has both required scopes

The cf-access stack manages two distinct CF resource categories that require different token scopes:

Category Resources Required token scope
CF Zero Trust Access applications, policies, service tokens Account:Zero Trust:Edit
Zone WAF rulesets cloudflare_ruleset.freescout_lambda_skip Zone:Firewall Services:Edit

Neither of the existing tokens covers both: - CLOUDFLARE_ACCESS_MGMT_TOKEN: Account:Zero Trust:Edit only - CLOUDFLARE_RAXX_AUTOMATION_API_TOKEN: Zone:Firewall Services:Edit only (limited)

Fix: Mint a new combined-scope token in the CF dashboard:

https://dash.cloudflare.com/profile/api-tokens
→ Create Token → Custom token
Permissions:
  - Account > Zero Trust > Edit
  - Zone > Firewall Services > Edit  (Resource: raxx.app zone)

Store the new token in Infisical at path /MooseQuest/cloudflare/ as CF_ACCESS_AND_WAF_TOKEN (or update CLOUDFLARE_ACCESS_MGMT_TOKEN scope in-place).

Blocker 2: CF provider v4.x bug with cloudflare_zero_trust_access_policy import

Provider v4.52.7 (the version pinned in versions.tf) has a bug where the ReadResource call during terraform import of cloudflare_zero_trust_access_policy fails with:

Error: error determining resource: "zone_id" or "account_id" required

Root cause: the provider's ImportState function sets only the resource UUID, then calls Read. The Read function calls helpers.GetAccountOrZoneID(d, ...) which reads account_id from the resource data — but during import, the config hasn't been applied to the state yet, so the attribute is empty.

This bug affects both CLI import (terraform import) and HCL import blocks.

Workaround options (operator chooses one):

A. Upgrade provider to v5.x (breaking change — see https://github.com/cloudflare/terraform-provider-cloudflare/blob/master/MIGRATION.md). Resource types and attributes change significantly. File a separate ticket.

B. Use terraform state push after manually constructing state JSON entries. Safe only when operator reviews + approves the state JSON before push. File #1864 action item to authorize this as a one-time operation.

C. Accept that console_operator and vault_operator policies will be CREATED (not imported). Risk: CF would receive duplicate "Operator only" policies at precedence 1. After creation, manually delete the pre-existing duplicate via the CF dashboard, then re-import the new resource. Net effect is safe but operationally messy.

Recommended: Option A (provider upgrade) as the durable fix. Option B for the immediate unblock if the operator needs state clean in <1 week.


Resources requiring import

All IDs verified via CF API calls on 2026-05-12 UTC.

1. cloudflare_zero_trust_access_policy.console_operator

What it is: "Operator only" (allow + email OTP) policy on console.raxx.app. Pre-existing policy created via CF dashboard before this stack was in Terraform.

Account:  22b5c35090724fbf05db6d4f501ac821
App ID:   0b55d01b-592b-4da4-b170-02d48a9f550d   (Raxx Console)
Policy:   fbe9a41d-b829-472a-9fb6-9b3dbfced820

Import command (once Blocker 2 is resolved):

# Using import block in imports.tf (preferred, already present):
terraform plan   # will show import + 0 create for this resource

# Alternatively, CLI import after provider upgrade to v5:
terraform import \
  cloudflare_zero_trust_access_policy.console_operator \
  "22b5c35090724fbf05db6d4f501ac821/0b55d01b-592b-4da4-b170-02d48a9f550d/fbe9a41d-b829-472a-9fb6-9b3dbfced820"

Verification:

terraform state show cloudflare_zero_trust_access_policy.console_operator
# Expect: decision=allow, name="Operator only", precedence=1

2. cloudflare_zero_trust_access_policy.vault_operator

What it is: "Operator only" policy on vault.raxx.app. Same pattern as above.

Account:  22b5c35090724fbf05db6d4f501ac821
App ID:   e4d07709-35b7-4698-97b6-32eb7bc7fb5a   (Infisical Vault)
Policy:   b29b191e-e8e8-4463-8604-3414e26decf0

Import command (once Blocker 2 is resolved):

terraform import \
  cloudflare_zero_trust_access_policy.vault_operator \
  "22b5c35090724fbf05db6d4f501ac821/e4d07709-35b7-4698-97b6-32eb7bc7fb5a/b29b191e-e8e8-4463-8604-3414e26decf0"

Verification:

terraform state show cloudflare_zero_trust_access_policy.vault_operator
# Expect: decision=allow, name="Operator only", precedence=1

3. cloudflare_zero_trust_access_application.infisical_cloud_saas

Status: NOT an import — this is a NEW resource.

Despite the original #1864 description, the infisical_cloud_saas SaaS-type CF Access application does NOT exist in the live CF account as of 2026-05-12. Confirmed via:

curl -sS -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/22b5c35090724fbf05db6d4f501ac821/access/apps" \
  | python3 -c "import sys,json; [print(a['id'],a['name'],a['type']) for a in json.load(sys.stdin).get('result',[])]"
# No SaaS-type app or "Infisical Cloud" app in the results.

This resource will be CREATED on first terraform apply. That is correct behavior.


4. cloudflare_zero_trust_access_policy.infisical_cloud_operator

Status: NOT an import — this is a NEW resource.

Depends on cloudflare_zero_trust_access_application.infisical_cloud_saas which doesn't exist yet. Will be CREATED when the parent app is created. Correct behavior.


5. cloudflare_ruleset.freescout_lambda_skip

What it is: Zone-level http_request_firewall_custom ruleset for raxx.app. Contains the Bot Fight Mode skip rule for CF-Access-authenticated Lambda traffic.

Zone:     f12dbb5cac57d5591a5058874498a6d1   (raxx.app)
Ruleset:  17dc768ccadf4d02ae279e133b7b5bfd
Rule:     c8c0b91d4e2a4f99bc62237ad6a498b9

Import command (requires combined-scope token from Blocker 1):

# HCL import block (already in imports.tf):
# import {
#   to = cloudflare_ruleset.freescout_lambda_skip
#   id = "zones/f12dbb5cac57d5591a5058874498a6d1/17dc768ccadf4d02ae279e133b7b5bfd"
# }

# OR CLI import:
terraform import cloudflare_ruleset.freescout_lambda_skip \
  "zones/f12dbb5cac57d5591a5058874498a6d1/17dc768ccadf4d02ae279e133b7b5bfd"

Note on import ID format: The CF provider v4.52.7 uses resourceLevel/resourceIdentifier/rulesetID format (e.g. zones/<zone_id>/<ruleset_id>), NOT the legacy <zone_id>/<ruleset_id> format. The freescout_service_token.tf comment block uses the old format — both docs and imports.tf have been corrected in #1864.

Verification:

terraform state show cloudflare_ruleset.freescout_lambda_skip
# Expect: zone_id=f12dbb5cac57d5591a5058874498a6d1, phase=http_request_firewall_custom
# Expect: rules[0].action=skip, rules[0].ref=c8c0b91d4e2a4f99bc62237ad6a498b9

Full apply sequence (once both blockers resolved)

# 1. Set the combined-scope token
export CLOUDFLARE_API_TOKEN=$(infisical secrets get CF_ACCESS_AND_WAF_TOKEN \
  --path /MooseQuest/cloudflare/ --plain)

# 2. Set zone ID (non-secret)
export TF_VAR_cf_zone_id="f12dbb5cac57d5591a5058874498a6d1"

# 3. Set AWS creds for S3 backend
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_DEFAULT_REGION="us-east-1"

cd terraform/cf-access

# 4. Init (if not already done)
terraform init

# 5. Plan — expect: 3 imports, 2 creates (infisical_cloud_saas + policy), 0 destroys
#    Plus 1 in-place update (freescout_api app minor diff) and 1 destroy (google IdP
#    count=0 because cf_google_workspace_client_id is empty — expected)
terraform plan

# 6. Apply
terraform apply

# 7. Verify clean state
terraform plan   # must show: No changes. Your infrastructure matches the configuration.

Code fixes included in this PR (#1864)

All code changes are in the PR branch sc-tf-cf-access-state-drift-1864.

Fix 1: freescout_service_token.tf line 123

decision = "allow" corrected to decision = "non_identity" to match live CF state. Without this fix, a terraform apply would revert the live Lambda→FreeScout pipeline to allow, breaking machine-identity authentication. See feedback_cf_access_service_token_needs_non_identity.md.

Fix 2: terraform.tfvars account_id

cf_access_account_id changed from placeholder string "REPLACE_WITH_TF_VAR_CF_ACCESS_ACCOUNT_ID_MOOSEQUEST" to the real account ID "22b5c35090724fbf05db6d4f501ac821" (non-secret — visible in CF dashboard URL and all API responses). This fixes the tfvars-vs-env-var precedence trap (tfvars takes precedence over TF_VAR_* env vars, so the placeholder was silently used).

Fix 3: vault_google_idp.tf resource type

cloudflare_zero_trust_identity_provider corrected to cloudflare_zero_trust_access_identity_provider. The former doesn't exist in provider v4.52.7 and caused Invalid resource type errors on every terraform plan. Same fix applied to outputs.tf which referenced the old type.


How to tell the stack is broken

Escalation

Wake the operator when: - terraform apply proposes destroying any existing CF Access application or policy - The combined-scope token has been minted and the operator wants to authorize the apply - The terraform state push approach (Blocker 2 Option B) is chosen — requires operator review of the state JSON before push

References