Skip to main content

Context

ADR-0018 established the pattern: one cloudflare_turnstile_widget resource per environment in iac-access/foundation, with the widget's .secret attribute piped automatically to GCP Secret Manager. All environments — dev, qa, and prd — were intended to use real widget keys that validate challenges against the Cloudflare network.

Plan 163 (Turnstile unification) removed the last dev-mode bypass from svc-access. After that change, TURNSTILE_SECRET_KEY is required unconditionally at startup in all environments. A missing or empty secret causes an immediate os.Exit(1) crash-loop before the service accepts any traffic. This made the secret a hard prerequisite for every promote, not just prd.

Simultaneously, the team needed a way for dev and qa environments to run end-to-end tests — including the full Turnstile validation path — without consuming the quota of a real widget or generating noise in Cloudflare analytics. Cloudflare publishes a pair of always-pass test keys for exactly this purpose:

KeyValue
Sitekey (public)1x00000000000000000000BB
Secret key1x0000000000000000000000000000000AA

These keys are documented publicly by Cloudflare and intentionally designed for dev/CI use. They always pass Turnstile verification — no real challenge is issued. They must never be used in production.

Decision

Add a use_test_turnstile_keys boolean variable (default: false) to iac-access/foundation. When true, the google_secret_manager_secret_version for access-turnstile-secret-key is populated with the Cloudflare public test key instead of the real widget secret.

Concretely:

  1. New Terraform variablevar.use_test_turnstile_keys (type bool, default false) in iac-access/foundation/variables.tf. No other IaC layer or application code needs to change.
  2. Conditional secret valuesecret_data = var.use_test_turnstile_keys ? "1x0000000000000000000000000000000AA" : cloudflare_turnstile_widget.access.secret in secrets.tf. The real widget is always created regardless of this toggle — widget creation is free and avoiding it would add unnecessary conditional complexity.
  3. Terraform precondition guard — the google_secret_manager_secret_version resource carries a precondition block that aborts terraform plan with a clear error if var.use_test_turnstile_keys == true and var.iac_environment == "prd". The error message explicitly states that test keys must not be used in production. This guard is enforced at plan time, not apply time.
  4. Sitekey for Workers Builds stays a manual step — as established in ADR-0018, the Cloudflare provider does not expose Workers Builds environment variables as Terraform resources. After applying with use_test_turnstile_keys = true, operators must manually set PUBLIC_TURNSTILE_SITE_KEY = 1x00000000000000000000BB in the CF Dashboard (Worker → Settings → Builds → Build variables) for dev/qa Workers. This is the same manual step required for real keys.
  5. Default value by environment — dev and qa tfvars set use_test_turnstile_keys = true; prd tfvars set use_test_turnstile_keys = false (enforced additionally by the precondition).

Why a new ADR rather than updating ADR-0018

ADR-0018 is Accepted and therefore immutable. Modifying an accepted ADR would distort the historical record of what was decided and when. This decision extends the original pattern without invalidating it — a new ADR is the correct form.

Consequences

  • Dev and qa can run end-to-end tests through the full Turnstile validation path without requiring real challenge solutions or consuming widget quota.
  • Cloudflare analytics in dev/qa are clean — test-key validations do not appear as real traffic in the Turnstile dashboard.
  • Production is protected by two independent guards: the precondition block that fails the plan, and the explicit false value in prd tfvars. Accidentally deploying test keys to prd requires overriding both.
  • svc-access has no awareness of this toggle — the application reads TURNSTILE_SECRET_KEY from Secret Manager at startup as before. The test key is indistinguishable from a real key at the application level, which means the integration tests exercise the real code path end-to-end.
  • Dev/qa do not detect real widget configuration problems: since test keys always pass, a misconfigured widget (wrong domain, wrong mode) will not surface in automated tests. At least one manual smoke test with real keys is recommended before promoting to prd.
  • Apply sequence is unchanged but more visible: iac-access/foundation must still be applied before deploying svc-access in any environment. The startup guard (see Gotcha #8) makes this requirement hard to miss.
  • Toggle is reversible: setting use_test_turnstile_keys = false on the next apply restores the real widget secret to Secret Manager. No service restart is needed beyond the normal rolling deploy triggered by the change.

Alternatives considered

  • Environment-specific Terraform modules with conditional widget creation: would avoid creating a real widget in dev/qa. Rejected — widgets are free, the conditional logic adds complexity, and the existing single-widget-per-environment model is simpler to operate.
  • Keep the dev-mode bypass in svc-access (pre-Plan 163 approach): the application would skip Turnstile validation when APP_ENV != "production". Rejected in Plan 163 — the bypass was a code-level security shortcut that diverged dev and prd behavior. All environments must exercise the same code path.
  • TURNSTILE_SECRET_KEY optional at startup (soft startup guard): the service starts with a degraded state if the secret is missing, logging a warning instead of exiting. Rejected — silent degradation in dev would mask configuration errors. Fail-fast is the correct behavior; operators must provision the secret before deploying.
  • Shared test key stored as a separate Terraform local value (no variable): simpler — no variable to set. Rejected — it would make test-key usage invisible in the environment's tfvars file. Explicit variable makes the intent auditable in plan diffs.

References