diff --git a/.github/workflows/terraform-apply-production.yml b/.github/workflows/terraform-apply-production.yml index 415670e..e4322b4 100644 --- a/.github/workflows/terraform-apply-production.yml +++ b/.github/workflows/terraform-apply-production.yml @@ -38,7 +38,8 @@ env: TF_VERSION: '1.9.8' TF_IN_AUTOMATION: 'true' TF_ENV: 'production' - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} + CLOUDFLARE_API_KEY: ${{ secrets.CLOUDFLARE_API_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_R2_SECRET_ACCESS_KEY }} AWS_REGION: 'auto' @@ -108,7 +109,8 @@ jobs: - name: Verify operator secrets are set run: | missing="" - [ -z "${CLOUDFLARE_API_TOKEN}" ] && missing="${missing} CLOUDFLARE_API_TOKEN" + [ -z "${CLOUDFLARE_EMAIL}" ] && missing="${missing} CLOUDFLARE_EMAIL" + [ -z "${CLOUDFLARE_API_KEY}" ] && missing="${missing} CLOUDFLARE_API_KEY" [ -z "${AWS_ACCESS_KEY_ID}" ] && missing="${missing} TF_STATE_R2_ACCESS_KEY_ID" [ -z "${AWS_SECRET_ACCESS_KEY}" ] && missing="${missing} TF_STATE_R2_SECRET_ACCESS_KEY" [ -z "${CF_ACCOUNT_ID}" ] && missing="${missing} CF_ACCOUNT_ID" diff --git a/.github/workflows/terraform-apply-staging.yml b/.github/workflows/terraform-apply-staging.yml index 2e8ef76..bceb50b 100644 --- a/.github/workflows/terraform-apply-staging.yml +++ b/.github/workflows/terraform-apply-staging.yml @@ -34,7 +34,10 @@ env: TF_VERSION: '1.9.8' TF_IN_AUTOMATION: 'true' TF_ENV: 'staging' - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + # Global Key via EMAIL+KEY env vars (CLOUDFLARE_API_TOKEN forces Bearer + # which Global Keys fail on Rulesets/R2/account-scoped endpoints — 9106). + CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} + CLOUDFLARE_API_KEY: ${{ secrets.CLOUDFLARE_API_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_R2_SECRET_ACCESS_KEY }} AWS_REGION: 'auto' @@ -72,7 +75,8 @@ jobs: - name: Verify operator secrets are set run: | missing="" - [ -z "${CLOUDFLARE_API_TOKEN}" ] && missing="${missing} CLOUDFLARE_API_TOKEN" + [ -z "${CLOUDFLARE_EMAIL}" ] && missing="${missing} CLOUDFLARE_EMAIL" + [ -z "${CLOUDFLARE_API_KEY}" ] && missing="${missing} CLOUDFLARE_API_KEY" [ -z "${AWS_ACCESS_KEY_ID}" ] && missing="${missing} TF_STATE_R2_ACCESS_KEY_ID" [ -z "${AWS_SECRET_ACCESS_KEY}" ] && missing="${missing} TF_STATE_R2_SECRET_ACCESS_KEY" [ -z "${CF_ACCOUNT_ID}" ] && missing="${missing} CF_ACCOUNT_ID" diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index fe9cc2c..693a808 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -74,7 +74,11 @@ jobs: working-directory: terraform/cloudflare # CF creds + state-backend creds passed in via env, not inlined in run:. env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + # Global Key via EMAIL+KEY env vars (provider uses X-Auth-* headers). + # NOT CLOUDFLARE_API_TOKEN — that's Bearer-only and Global Keys fail + # Bearer auth on Rulesets / R2 / account-scoped endpoints (9106). + CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} + CLOUDFLARE_API_KEY: ${{ secrets.CLOUDFLARE_API_KEY }} AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_R2_SECRET_ACCESS_KEY }} AWS_REGION: 'auto' @@ -95,7 +99,8 @@ jobs: # pointing at the README and the exact missing variable names. run: | missing="" - [ -z "${CLOUDFLARE_API_TOKEN}" ] && missing="${missing} CLOUDFLARE_API_TOKEN" + [ -z "${CLOUDFLARE_EMAIL}" ] && missing="${missing} CLOUDFLARE_EMAIL" + [ -z "${CLOUDFLARE_API_KEY}" ] && missing="${missing} CLOUDFLARE_API_KEY" [ -z "${AWS_ACCESS_KEY_ID}" ] && missing="${missing} TF_STATE_R2_ACCESS_KEY_ID" [ -z "${AWS_SECRET_ACCESS_KEY}" ] && missing="${missing} TF_STATE_R2_SECRET_ACCESS_KEY" [ -z "${CF_ACCOUNT_ID}" ] && missing="${missing} CF_ACCOUNT_ID" @@ -104,7 +109,7 @@ jobs: echo "::error::See https://github.com/InstaNode-dev/infra/blob/master/terraform/cloudflare/README.md#bootstrap-one-time" exit 1 fi - echo "all 4 operator secrets present" + echo "all 5 operator secrets present" - name: terraform init run: | diff --git a/terraform/cloudflare/dns.tf b/terraform/cloudflare/dns.tf index fbfd48c..9f3d9c4 100644 --- a/terraform/cloudflare/dns.tf +++ b/terraform/cloudflare/dns.tf @@ -1,12 +1,14 @@ # DNS records under management. # # Pre-cutover ritual (D-3): TTL must be 60s for ≥48h BEFORE any cut. -# Setting it that low here means terraform plan/apply itself satisfies -# the pre-step the first time we touch the record. +# That ONLY applies to grey-cloud (proxied=false) records — CF requires +# proxied=true records to have ttl=1 (CF manages TTL internally; setting +# 60 returns a 400 "ttl must be set to 1 when `proxied` is true"). # -# `proxied = true` = CF orange-cloud; `false` = grey-cloud (DNS only, no -# proxy). Today: marketing apex is orange (Phase 0 baseline), api is grey -# (becomes orange in Phase 4 — flip this flag in that phase's PR). +# `proxied = true` = CF orange-cloud (ttl=1); `false` = grey-cloud, +# DNS only (ttl=60 for cutover ramp). Today: marketing apex is orange +# (Phase 0 baseline), api is grey (becomes orange in Phase 4 — flip +# both the proxied flag AND ttl=60→1 in that phase's PR). locals { marketing_origin = "instanode-web.pages.dev" # set per environment in staging.tfvars / production.tfvars after Pages project is created @@ -18,7 +20,7 @@ resource "cloudflare_dns_record" "apex" { name = var.zone_name type = "CNAME" content = local.marketing_origin - ttl = 60 + ttl = 1 proxied = true comment = "marketing apex; CNAME-flattened to Pages project" } @@ -28,7 +30,7 @@ resource "cloudflare_dns_record" "www" { name = "www.${var.zone_name}" type = "CNAME" content = var.zone_name - ttl = 60 + ttl = 1 proxied = true comment = "www → apex redirect handled by CF page rule" } @@ -49,7 +51,7 @@ resource "cloudflare_dns_record" "staging" { name = "staging.${var.zone_name}" type = "CNAME" content = "instant-staging.${var.zone_name}.cdn.cloudflare.net" # Pages preview hostname; replaced after Pages project is up - ttl = 60 + ttl = 1 proxied = true comment = "staging mirror per D-2" } diff --git a/terraform/cloudflare/staging.tf b/terraform/cloudflare/staging.tf index 10deda6..08bc26a 100644 --- a/terraform/cloudflare/staging.tf +++ b/terraform/cloudflare/staging.tf @@ -48,7 +48,7 @@ resource "cloudflare_dns_record" "staging_wildcard" { # regardless of what's here. A 404 sink is intentional — any unrouted # subdomain hits CF's default 404 page. content = local.staging_stem - ttl = 60 + ttl = 1 proxied = true comment = "wildcard for per-tenant CF Container services in staging; routed via cloudflare_workers_route below" } @@ -70,7 +70,7 @@ resource "cloudflare_dns_record" "staging_deployment_wildcard" { name = "*.deployment.${local.staging_stem}" type = "CNAME" content = "deployment.${local.staging_stem}" - ttl = 60 + ttl = 1 proxied = true comment = "wildcard for /deploy/new staging apps (mirrors prod *.deployment.instanode.dev)" } @@ -83,7 +83,7 @@ resource "cloudflare_dns_record" "staging_deployment_anchor" { name = "deployment.${local.staging_stem}" type = "AAAA" content = "100::" # IPv6 discard prefix — never reachable; CF proxied front-end terminates - ttl = 60 + ttl = 1 proxied = true comment = "anchor for deployment wildcard CNAME (CF requires a real record at the parent)" } @@ -102,7 +102,7 @@ resource "cloudflare_dns_record" "staging_webhook" { name = "webhook.${local.staging_stem}" type = "AAAA" content = "100::" # placeholder; CF orange-cloud handles routing - ttl = 60 + ttl = 1 proxied = true comment = "staging /webhook/new receiver subdomain" } @@ -122,7 +122,7 @@ resource "cloudflare_dns_record" "staging_dashboard" { name = "dashboard.${local.staging_stem}" type = "CNAME" content = "instanode-dashboard-staging.pages.dev" # set after dashboard Pages project is created - ttl = 60 + ttl = 1 proxied = true comment = "staging dashboard — QA-only; D-5 keeps prod dashboard off Pages" }