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:
| Key | Value |
|---|---|
| Sitekey (public) | 1x00000000000000000000BB |
| Secret key | 1x0000000000000000000000000000000AA |
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:
- New Terraform variable —
var.use_test_turnstile_keys(typebool, defaultfalse) iniac-access/foundation/variables.tf. No other IaC layer or application code needs to change. - Conditional secret value —
secret_data = var.use_test_turnstile_keys ? "1x0000000000000000000000000000000AA" : cloudflare_turnstile_widget.access.secretinsecrets.tf. The real widget is always created regardless of this toggle — widget creation is free and avoiding it would add unnecessary conditional complexity. - Terraform
preconditionguard — thegoogle_secret_manager_secret_versionresource carries apreconditionblock that abortsterraform planwith a clear error ifvar.use_test_turnstile_keys == trueandvar.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. - 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 setPUBLIC_TURNSTILE_SITE_KEY = 1x00000000000000000000BBin the CF Dashboard (Worker → Settings → Builds → Build variables) for dev/qa Workers. This is the same manual step required for real keys. - Default value by environment — dev and qa tfvars set
use_test_turnstile_keys = true; prd tfvars setuse_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
preconditionblock that fails the plan, and the explicitfalsevalue in prd tfvars. Accidentally deploying test keys to prd requires overriding both. svc-accesshas no awareness of this toggle — the application readsTURNSTILE_SECRET_KEYfrom 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/foundationmust still be applied before deployingsvc-accessin any environment. The startup guard (see Gotcha #8) makes this requirement hard to miss. - Toggle is reversible: setting
use_test_turnstile_keys = falseon 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 whenAPP_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_KEYoptional 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
- ADR-0018 — Cloudflare Turnstile widget managed in Terraform with secret pipe to Secret Manager (this ADR extends ADR-0018)
- Plan 163:
.plans/done/163-turnstile-unification/ - Cloudflare Turnstile — Testing — official source for the always-pass test keys
- Environment Promotion Runbook — Gotcha #8 — crash-loop when
iac-access/foundationhas not run before deployingsvc-access .knowledge/tools.mdentry T1 — Cloudflare Turnstile always-pass test keys