ADR-0017: Gate every dev.* hostname behind CF Access + Google Workspace SSO¶
- Status: Accepted — implemented 2026-06-06 (10 CF Access resources added to envs/dev: 5 apps + 5 policies; total Access resources now 18)
- Date: 2026-06-06
- Deciders: cloud-architect, secops-agent, cloudflare-expert, marnissi.investments
- Supersedes: ADR-0013 §CF Access (partial — only the public-frontend-dev exemption block)
Context¶
ADR-0013 §CF Access (Existing dev environment naming hygiene + the §CF Access on the dev tier section) declared that the public-facing dev frontends (dev.marnissi-holdings.com and dev.quantmods.com) would be public, not behind Access — same posture as their prod siblings (apex marnissi-holdings.com + apex quantmods.com). The reasoning at the time was symmetry: a hostname's auth posture should mirror its prod sibling's posture. Internal backends (admin, api, labs) had Access apps in both dev and prod. Public frontends had none in either tier.
That symmetry decision predated the actual first dev frontend going live. With dev.quantmods.com deployed 2026-06-06 (serving the ComingSoonPage from the quantmods-website-dev Worker + bucket), the practical consequence surfaced: a dev hostname publishes whatever the develop branch has built, including half-finished UI iteration, pre-launch copy, and any error pages produced by misconfigurations. Public exposure of that surface is:
- Brand risk. Search engines index dev URLs they discover. Pre-launch copy and broken builds appearing in search results pre-launch damages the launch narrative.
- Iteration drag. A public dev tier forces a posture of "every push must be presentable" — exactly what a dev tier is meant to relax. The point of a dev/prod split is the freedom to iterate without the cost of public visibility.
- No real operational need. The dev surface has zero external users by design. Operator-only access via the existing CF Access + Google Workspace flow has zero recurring marginal cost (Free tier 50 seats, same
MGH Orggroup, same policy shape) and adds a single SSO redirect on first visit.
The decision is to reverse ADR-0013's public-frontend-dev stance and gate every dev.* hostname uniformly. Prod public-facing frontends (apex marnissi-holdings.com + apex quantmods-holdings.com) stay open — they exist to be public.
Decision¶
All dev.* hostnames across both CF zones — including the public-frontend dev variants — are CF Access self-hosted applications protected by the existing MGH Org group policy (Google Workspace OIDC IdP, allowed_email_domain = marnissi-holdings.com, decision = allow).
The full set of protected dev hostnames is:
| Hostname | Brand | Worker / origin | Access app key |
|---|---|---|---|
admin.dev.marnissi-holdings.com |
MGH | tunnel → admin-ui dev container | admin_dev |
api.dev.marnissi-holdings.com |
MGH | tunnel → admin-api dev container | api_dev |
labs.dev.marnissi-holdings.com |
MGH | tunnel → labs dev container | labs_dev |
dev.marnissi-holdings.com |
MGH | Worker mgh-website-dev → bucket mgh-website-dev (not yet deployed) |
mgh_website_dev |
dev.quantmods.com |
Quantmods | Worker quantmods-website-dev → bucket quantmods-website-dev (live 2026-06-06) |
quantmods_website_dev |
Prod hostnames retain the ADR-0008 posture: internal backends (admin / api / labs) behind Access; public frontends (apex marnissi-holdings.com + apex quantmods.com) open. mgh-docs.pages.dev remains public per ADR-0014.
Implementation lives in modules/cloudflare-access/main.tf — the local.protected_apps map is extended with five new keys. Each key produces a cloudflare_zero_trust_access_application + a cloudflare_zero_trust_access_policy.allow_mgh_org[<key>]. Both resource types are account-scoped (no zone_id), so a single module call spans both CF zones (marnissi-holdings.com + quantmods.com) — no per-brand split needed.
Consequences¶
-
Cost (delta vs free tier): $0. CF Access Free tier covers 50 seats; the operator headcount is unchanged. Each new Access app + policy pair is a free-tier resource. No new SaaS surface.
-
Operational surface:
- 10 new Tofu-managed resources (5 apps + 5 policies). The
cloudflare-accessmodule live resource count rises from 8 to 18. - First visit to any dev hostname from a clean browser hits the CF Access login screen → Google OAuth → return to origin. Session lifetime 24h (
var.session_duration). Subsequent visits within the window are transparent. -
Service-token bypass remains available for CI smoke tests if needed — not added today (operator preference: pure human-SSO until a CI smoke-test requirement materialises).
secops-agentchecklist still applies if a token is ever minted. -
Security posture:
- Dev tier now matches prod backend tier on auth — same IdP, same group, same policy decision. Reduces the question "is this hostname public?" to a single answer ("only
marnissi-holdings.com+www,quantmods.com+www, andmgh-docs.pages.devare public") and makes drift detection trivial. - The existing
secops-agentaudit hook ("on every infra PR that adds a new subdomain, verify it appears in the relevant zone's WAFhostname_allowlistand, if internal-facing, has a corresponding CF Access app" — ADR-0013 §Security posture) is materially simpler now: every newdev.*subdomain gets an Access app by default, no per-hostname exception list. -
No HSTS / WAF change required — the WAF
hostname_allowlistextension to*.dev.*from ADR-0013 §CF WAF stays as authored. Managed Ruleset evaluation continues for dev traffic; the Access redirect happens at the CF edge before WAF skip rules apply. -
Migration path if we revisit: If a dev hostname must be temporarily public (e.g. a stakeholder preview for a sales conversation), drop its key from
local.protected_appsand apply — Tofu destroys the Access app + policy, hostname serves public immediately. Re-add and apply when done. Estimated effort: minutes. No data migration. The reverse path (re-add Access on a hostname that has been public for a while) likewise requires only atofu applyplus a single SSO login by each operator.
Alternatives considered¶
| Option | Why rejected |
|---|---|
| Keep ADR-0013's public dev frontend posture | The cost of public dev frontends (brand-risk + iteration-drag) is real and recurring; the cost of moving them behind SSO is one Tofu apply + a single browser redirect on first visit per operator per 24h. The trade is overwhelmingly in favour of gating. |
| Use a separate "low-trust dev" Access group | Adds policy surface without operational benefit — the operator headcount is small, and a separate group would not gate the dev hostnames against any group member that is already trusted with prod backend access. The single MGH Org group remains the cleanest model. |
noindex meta tag + robots.txt instead of Access |
Mitigates search-index leakage but not the underlying public-exposure problem. A pre-launch URL can still be linked, screenshotted, scraped, or hit by drive-by automated traffic. Access is a stronger primitive for the same goal at the same cost. |
| Cloudflare Access "Bypass" service token, dev-only | Service tokens defeat the SSO posture for any holder of the token. Not pursued unless / until CI smoke tests need an automation path that can't use a human session. |
| Tightening at the brand level (gate quantmods. but not marnissi.) | The brand asymmetry would be confusing and unprincipled. Both brands have the same operator surface; both should follow the same posture. |