Skip to content

ADR-0009: Cloudflare Email Routing — inbound catch-all forwarded to operator Gmail

  • Status: Accepted
  • Date: 2026-05-17
  • Deciders: cloud-architect, cloudflare-expert, secops-agent, finops-agent, marnissi.investments

Context

MGH needs inbound email on marnissi-holdings.com. Today the organisation is single-operator; all operational mail (DMARC aggregate reports, vendor notifications, ad-hoc contact) should land in the operator's existing inbox at marnissi.investments@gmail.com. No internal mail server, no additional mailbox license, and no secondary email platform is warranted at this scale.

Cloudflare Email Routing is already listed in infra/CLAUDE.md §Stack and §Hostname catalog as the designated inbound backend. It is free-tier unlimited (rules and message volume, per infra/finops/ledger.md). Critical (cloudflare-expert R1): Cloudflare's Enable Email Routing API auto-creates and locks the MX records (amir/linda/isaac.mx.cloudflare.net) AND adds an include:_spf.mx.cloudflare.net SPF entry. Neither Tofu module owns those records — they are CF-managed. This module enables routing, declares the destination, and adds the forwarding rule only; the DNS module (ADR-0006) consumes the SPF entry via the §SPF merge import procedure to combine it with OCI's outbound include in a single TXT.

The DMARC rua=mailto:dmarc-reports@marnissi-holdings.com record (established in ADR-0005/ADR-0006) routes aggregate reports through the catch-all defined here, delivering them to the operator inbox via the same forwarding path.

Outbound transactional mail (no-reply@marnissi-holdings.com) is a separate concern handled by OCI Email Delivery (ADR-0005). This ADR covers inbound only.

Decision

Enable Cloudflare Email Routing on the marnissi-holdings.com zone and add one catch-all forwarding rule. The modules/cloudflare-email-routing/ Tofu module manages the zone-level routing toggle, the destination address, and the catch-all rule. MX records and the CF auto-SPF entry are created and locked by Cloudflare on enable — neither Tofu module owns them in state.

Routing rule table

Matcher Type Value Action Destination
Catch-all all *@marnissi-holdings.com forward marnissi.investments@gmail.com

Destination address verification — manual operator step. Cloudflare sends a one-time verification email to marnissi.investments@gmail.com when the destination is first registered. The operator must click the confirmation link before the forwarding rule becomes active. This step cannot be automated via Tofu or the Cloudflare API. It must be completed before the module is considered operationally live. infra-agent must document this as a numbered step in infra/scripts/bootstrap-oci.md (or an equivalent bootstrap runbook section) under a heading such as §Step 11 — Verify CF Email Routing destination.

Inter-module dependency. modules/cloudflare-dns/ depends on modules/cloudflare-email-routing/ having been applied first (CF must own the auto-created MX + auto-SPF before the DNS module reconciles its own SPF via the import procedure). envs/dev/main.tf enforces this via depends_on = [module.cloudflare_email_routing] on the DNS module call.

Consequences

  • Cost (delta vs free tier): $0. CF Email Routing inbound forwarding and routing rules are unlimited on the Free plan. No cost cliff exists for this capability. Ledger row CF Email Routing rules moves from 0 (not yet created) to 1.
  • Operational surface: One manual verification step (destination address confirmation) before go-live. No credentials to rotate — CF Email Routing operates entirely within the existing scoped CF API token used by the cloudflare/cloudflare Tofu provider. No new monitor required; volume is not surfaced in MGH observability tooling today (deferred to a future ADR if volume tracking becomes necessary).
  • Security posture: The catch-all rule forwards all mail addressed to any @marnissi-holdings.com address to the operator Gmail. This is intentional and correct for a single-operator company. secops-agent checklist: (a) confirm destination address marnissi.investments@gmail.com is verified before the module PR merges; (b) confirm no auto-forwarding loop exists (e.g. a Gmail filter that forwards back to a @marnissi-holdings.com address would create an infinite bounce cycle — operator must verify this is not configured); (c) DKIM/SPF/DMARC alignment is ensured by ADR-0006: forwarded messages carry CF's signed envelope and Gmail evaluates ARC (Authenticated Received Chain) for forwarded mail; no additional action required here. No new egress path, no new IAM scope, no new IdP.
  • Migration path if we revisit: Disable the catch-all rule and add per-prefix rules (admin@, billing@, etc.) pointing to different destinations, or replace CF Email Routing with a dedicated mailbox service (e.g. Google Workspace), by updating modules/cloudflare-email-routing/ only. If the receiving service requires different MX hosts, disabling cloudflare_email_routing_settings releases the CF-locked MX records and the modules/cloudflare-dns/ for_each map can then own MX entries explicitly. Reversibility is low-cost.

Alternatives considered

Option Why rejected
Per-prefix rules (admin@, billing@, dmarc-reports@ → separate destinations) Premature for a single-operator company. All mail goes to the same inbox today. Deferred to a future ADR when the team grows beyond one person and role-specific routing has value.
Forward to multiple Gmail accounts No second operator exists. Adds complexity and an additional verification step with no operational benefit now.
Route inbound to a dedicated mailbox service (e.g. Hey, Fastmail, Google Workspace Inbox) Introduces a paid subscription ($0 → $4–$12/user/month), a new credential to manage, and additional DNS complexity. CF Email Routing satisfies the requirement for free with no new operational surface.
Self-hosted mail server on OCI ARM VPS Consumed 4/4 OCPU and 24/24 GB RAM on the bootstrap VPS (per infra/finops/ledger.md). No headroom for an MTA. Operational burden (spam, blacklists, TLS, DKIM rotation) is disproportionate for inbound-only needs.