Skip to content

Cloudflare Operations

Operational guide for Cloudflare DNS, Tunnels, and Workers.

PropertyValue
DomainsAll 11 account zones under IaC (fzymgc.house, fzymgc.net, fzymgc.dev, fzymgc.email, plus bwcgroup.com, fovea-review.ai, fuzzymagic.com, holomush.dev, muxserver.net, seanb4t.dev, specgraph.io)
Terraform Moduletf/cloudflare/
API Token Pathsecret/fzymgc-house/infrastructure/cloudflare/*
Tunnel Namefzymgc-house-main
Workerhcp-terraform-discord
+----------------------------------------------------------------+
| Cloudflare Account |
+-----------------+-----------------+-----------------------------+
| fzymgc.house | fzymgc.net | Workers |
| (internal DNS) | (webhooks) | (serverless functions) |
+-----------------+-----------------+-----------------------------+
| Zero Trust Tunnel |
| fzymgc-house-main |
+----------------------------------------------------------------+

Uses a single hand-created bootstrap token that Terraform authenticates as. Account-owned workload tokens (e.g. the R2 S3 token in r2.tf) are created by Terraform via cloudflare_account_token (works on provider 5.21); the bootstrap token cannot bootstrap itself, so it stays manual.

TokenPurposePermissionsCreated By
BootstrapTerraform auth + all operationsAccount: Workers/R2/Tunnel/Access/API-Tokens; Zone (All zones in the account): DNS:Edit, Zone:Read, Zone Settings:EditManual; 1-year expiry

Blast radius: since Terraform is the source of truth for every zone’s DNS + settings, this token can edit DNS on all account domains (including personal ones sharing the account). It lives only in Vault, never in git.

PathContent
.../cloudflare/bootstrap-tokenBootstrap token for Terraform
.../cloudflare/discord-webhookDiscord webhook URL
.../cloudflare/hcp-terraform-hmacHMAC secret for webhook validation

Terraform is the source of truth for every Cloudflare DNS record and per-zone setting across all 11 account zones (adopted in hl-vzfh via cf-terraforming).

FileContents
tunnel.tfTunnel CNAMEs driven by for_each variables (*-wh.fzymgc.net, auth/id.fzymgc.house, status.fzymgc.net)
dns-<zone>.tfAll other records per zone (one file per zone), one cloudflare_dns_record resource + import{} block each
zone-settings.tfPer-zone cloudflare_zone_setting resources for settings explicitly changed from Cloudflare defaults

Records NOT managed by Terraform (by design)

Section titled “Records NOT managed by Terraform (by design)”

Do not import these — a controller owns them and TF management would conflict:

PatternOwner
_acme-challenge.* TXTcert-manager DNS-01 solver (argocd/app-configs/cert-manager-config/cloudflare-acme-issuer.yaml)
cf<year>-<n>._domainkey TXTCloudflare Email Routing (auto-generated DKIM)

Deprecated minify and transient development_mode zone settings are also left unmanaged.

  1. Tunnel-backed service: add to the relevant for_each variable in variables.tf (see webhook_services / internal_services).
  2. Any other record: add a cloudflare_dns_record resource to the matching dns-<zone>.tf (no import{} block — that’s only for adopting pre-existing records).
  3. Submit PR; the main-cluster-cloudflare HCP workspace plans, then applies on merge.

Adopting an existing (dashboard-created) record

Section titled “Adopting an existing (dashboard-created) record”

Generate faithful HCL + import blocks with cf-terraforming (provider v5), pointed at a throwaway provider-schema dir so you don’t need HCP auth to init:

Terminal window
export CLOUDFLARE_API_TOKEN=$(vault kv get -mount=secret -field=token \
fzymgc-house/infrastructure/cloudflare/bootstrap-token)
cf-terraforming generate --zone <zone_id> --resource-type cloudflare_dns_record \
--terraform-install-path <schema_dir> --modern-import-block

Verify zero-diff before merging by running terraform plan against live state with an empty local state — it must report N to import, 0 to add, 0 to change, 0 to destroy.

When a new domain is onboarded to the Cloudflare account:

  1. Token: no change needed. The bootstrap token is scoped All zones in the account, so any domain in this Cloudflare account is covered automatically. (Only relevant if the token were ever re-scoped to Specified Domains — then you’d add the new domain to its policy first.)
  2. Add records: create dns-<zone-slug>.tf (./-_). For an existing zone full of dashboard records, adopt them with cf-terraforming + import{} blocks as above; for a greenfield zone, write cloudflare_dns_record resources directly.
  3. Add changed settings to zone-settings.tf (only settings that differ from Cloudflare defaults — check modified_on != null via GET /zones/<id>/settings).
  4. Open a PR; confirm the HCP plan is zero-diff (imports) or intended (new records), then merge.

The zone itself is referenced as a data.cloudflare_zone (looked up by name), not managed as a cloudflare_zone resource — Terraform owns the records and settings, not zone creation/registration.

TypeUse Case
A / AAAADirect IP mapping
CNAMEAlias / tunnel target (<id>.cfargotunnel.com)
MX / TXTMail routing, SPF / DKIM / DMARC
CAACertificate authority authorization

The tunnel provides secure external access without exposing IPs.

Internet -> Cloudflare -> Tunnel -> Traefik -> Services

The primary tunnel fzymgc-house-main is defined in tunnel.tf (cloudflare_zero_trust_tunnel_cloudflared.main). Its ingress rules and the matching DNS CNAMEs (<id>.cfargotunnel.com) are generated from for_each variables in variables.tf — add a service by adding a map entry, not a raw record:

VariableHostname patternProtection
webhook_services<name>-wh.fzymgc.netPublic (webhook receivers)
internal_services<name>.fzymgc.netCloudflare Access
ssh_services<name>.fzymgc.netAccess browser-rendered SSH

The tunnel token is written to Vault (.../cloudflared/tunnels/<name>) and consumed by the cloudflared connector in argocd/app-configs/cloudflared-main/.

Adding a service to the existing tunnel is the common case (above). A new tunnel is only needed for a separate connector / isolation boundary:

  1. Add a cloudflare_zero_trust_tunnel_cloudflared resource + a cloudflare_zero_trust_tunnel_cloudflared_config (ingress) in tunnel.tf, and a data.cloudflare_zero_trust_tunnel_cloudflared_token to fetch its run token.
  2. Persist the token to Vault via a vault_kv_secret_v2 (mirror the existing tunnel_credentials resource).
  3. Point DNS cloudflare_dns_record CNAMEs at <new-tunnel-id>.cfargotunnel.com.
  4. Deploy a cloudflared connector (new argocd/app-configs/cloudflared-*/) that reads the Vault token.

There is currently a second, un-IaC’d tunnel d7ed1f27… backing wh-argowf/wh-events.fzymgc.house (its DNS records are managed; the tunnel object is not — tracked in hl-j4cv). Adopt it with cf-terraforming --resource-type cloudflare_zero_trust_tunnel_cloudflared when consolidating.

Transforms HCP Terraform notification webhooks into Discord embeds.

PropertyValue
Codecloudflare/workers/hcp-terraform-discord/worker.js
Terraformtf/cloudflare/workers.tf
SecretsDISCORD_WEBHOOK_URL, HMAC_SECRET

HMAC Validation: When HMAC_SECRET is configured, validates X-TFE-Notification-Signature header. Invalid signatures rejected with 401.

  1. Create token in Cloudflare Dashboard:

    • API Tokens > Create Token > Create Custom Token
    • Permissions:
      • Account > Workers Scripts > Edit
      • Account > Cloudflare Tunnel > Edit
      • Account > Account Settings > Read
      • Account > Account API Tokens > Edit (workload tokens in r2.tf)
      • Account > Workers R2 Storage > Edit
      • Account > Access: Apps and Policies > Edit; Access: Organizations, IdPs, and Groups > Edit
      • Zone > DNS > Edit
      • Zone > Zone > Read
      • Zone > Zone Settings > Edit (for zone-settings.tf)
    • Zone Resources: All zones from an account → (the fzymgc account). Do not use All Domains — an account-owned token rejects the all-zones wildcard with Account tag in access policy must match tag in request uri.
    • Expiration: set a 1-year expiry (not never-expire) and a rotation reminder.
  2. Store in Vault:

    Terminal window
    vault kv put secret/fzymgc-house/infrastructure/cloudflare/bootstrap-token \
    token="YOUR_BOOTSTRAP_TOKEN"
  3. Apply Terraform:

    Terminal window
    terraform -chdir=tf/cloudflare apply
tf/vault -> tf/cloudflare -> Configure HCP TF webhook
  1. tf/vault: Creates HMAC secret
  2. tf/cloudflare: Deploys Worker with HMAC binding, creates workload token
  3. HCP TF UI: Add HMAC token to notification webhook
  1. Check propagation: dig @1.1.1.1 service.fzymgc.house
  2. Verify Terraform state
  3. Check Cloudflare dashboard
Error: 403 Forbidden - Authentication error

Cause: API token missing required permissions, or the target zone is outside the token’s zone scope.

Fix:

  1. Check bootstrap token has all required permissions (see Bootstrap Token Setup)

  2. If a specific zone fails while others work, the token’s Specified Domains list is missing that zone — add it in the dashboard.

  3. Verify token is stored correctly in Vault:

    Terminal window
    vault kv get secret/fzymgc-house/infrastructure/cloudflare/bootstrap-token

Account tag in access policy must match tag in request uri

Section titled “Account tag in access policy must match tag in request uri”

Cause: Editing this account-owned token with an All Domains (all-zones wildcard) zone policy — Cloudflare requires the zone policy be pinned to the token’s account.

Fix: Use Specified Domains and list every zone explicitly instead of All Domains.

Terminal window
# Check tunnel status
kubectl -n cloudflared get pods
kubectl -n cloudflared logs -l app.kubernetes.io/name=cloudflared
# Verify credentials in Vault
vault kv get secret/fzymgc-house/cluster/cloudflared/tunnels/fzymgc-house-main
Terminal window
# Check Worker exists
curl -X GET "https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/workers/scripts" \
-H "Authorization: Bearer $(vault kv get -field=token secret/fzymgc-house/infrastructure/cloudflare/bootstrap-token)"
  • HCP Terraform Operations - Webhook notifications
  • Tunnel connector: argocd/app-configs/cloudflared-main/
  • Worker code: cloudflare/workers/hcp-terraform-discord/