Skip to content

ADR-0006: Cloudflare DNS baseline for marnissi-holdings.com (zone settings, CNAME topology, email auth records)

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

Context

MGH operates a single Cloudflare zone marnissi-holdings.com as the load-bearing edge for the entire stack. Every public and internal hostname listed in infra/CLAUDE.md §Hostname catalog terminates at this zone. Web traffic routes through CF Tunnel to the ARM A1 VPS (zero public ingress on OCI); email auth records (MX, SPF, DKIM, DMARC) in the same zone enforce deliverability and anti-spoofing for both the OCI Email Delivery outbound path (ADR-0005) and the Cloudflare Email Routing inbound path (ADR-0009).

No prior ADR has codified the zone-level security settings, the CNAME-only ingress topology, or the email auth record set. Without a single written baseline, individual records risk being added ad hoc, conflicting, or missing hardening settings. This ADR commits the full topology so that modules/cloudflare-dns/ has a clear, reviewable specification.

The modules/cloudflare-dns/ Tofu module sits in a dependency chain:

  • It depends on module.oci_email outputs dkim_cname_host and dkim_cname_value (ADR-0005, PR4 — must be applied first).
  • It depends on module.cloudflare_tunnel.tunnel_id (ADR-0007, same PR5 — tunnel UUID required for all CNAME targets).
  • It depends on module.cloudflare_email_routing having been applied first (depends_on guard). Resolved with cloudflare-expert (R1): CF auto-creates and locks MX records on cloudflare_email_routing_settings creation; neither Tofu module owns the MX records in state. See §Email auth records.

Provider authentication is via CLOUDFLARE_API_TOKEN (env var, never in .tfvars). The token must be scoped to: DNS Edit, Page Rules Edit, Email Routing Edit, DNSSEC Edit for zone marnissi-holdings.com only.

Decision

Implement modules/cloudflare-dns/ with the following specification. infra-agent authors the HCL; cloudflare-expert verifies provider resource names and attribute values before the PR is merged.

Zone hardening settings

Applied via the Cloudflare provider's zone settings resources. All settings target the single zone marnissi-holdings.com.

Setting Value Rationale
DNSSEC Enabled Prevents DNS hijacking; free on all CF plans
SSL/TLS mode full_strict OCI side already has a valid cert (or CF-issued origin cert); strict prevents MITM on the CF-to-origin leg through the Tunnel
Always Use HTTPS on Redirects all HTTP requests to HTTPS at the CF edge before they enter the Tunnel
Automatic HTTPS Rewrites on Upgrades mixed-content HTTP links in HTML responses
Minimum TLS Version 1.2 TLS 1.0 and 1.1 are deprecated; 1.3 preferred but 1.2 is the minimum to avoid breaking older clients
HSTS max_age=31536000; includeSubDomains; preload One year, all subdomains, preload list submission eligible
Security Level medium Default CF WAF challenge threshold; modules/cloudflare-waf/ (ADR-0008) may override per-route

secops note: DNSSEC and Minimum TLS 1.2 are non-negotiable. HSTS includeSubDomains and preload are mandatory from day one — retrofitting preload after subdomains are live is harder than setting it from the start.

Hostname-to-record mapping

All web hostnames CNAME to <tunnel-uuid>.cfargotunnel.com where <tunnel-uuid> is the output module.cloudflare_tunnel.tunnel_id (ADR-0007). No A records point to OCI public IPs. This is the architectural constraint that enforces zero public ingress on OCI.

CF proxied (orange-cloud) = true for all web CNAMEs — traffic enters the CF network, not bypasses it.

Hostname Record type Value CF proxied Access protected (ADR-0008)
marnissi-holdings.com (apex) CNAME (flattened) <tunnel-uuid>.cfargotunnel.com yes no (public)
www.marnissi-holdings.com Single Redirect rule 301 → https://marnissi-holdings.com/$1 n/a (rule, not record) no (public)
docs.marnissi-holdings.com CNAME <tunnel-uuid>.cfargotunnel.com yes no (public)
admin.marnissi-holdings.com CNAME <tunnel-uuid>.cfargotunnel.com yes yes
api.marnissi-holdings.com CNAME <tunnel-uuid>.cfargotunnel.com yes yes
labs.marnissi-holdings.com CNAME <tunnel-uuid>.cfargotunnel.com yes yes

Apex CNAME flattening: RFC 1033/1034 prohibits a CNAME at the zone apex because the apex must hold NS and SOA records. Cloudflare resolves this by synthesising an A/AAAA response from the CNAME target at query time ("CNAME flattening"). The Cloudflare provider resource cloudflare_record with type = "CNAME" at the apex is valid and produces this behaviour automatically when proxied. No special flag is required.

www redirect — Single Redirects, not Page Rules: CF Free tier provides 3 Page Rules. Single Redirects (Bulk Redirects in the provider) are unlimited on the Free plan. The www → apex redirect must be implemented as a Single Redirect rule to preserve Page Rule quota for future operational use.

status.marnissi-holdings.com is reserved (infra/CLAUDE.md §Hostname catalog). No record is created in this ADR. A placeholder comment in the module marks the intent.

Email auth records

All records live on zone apex marnissi-holdings.com unless the name field specifies otherwise. CF proxied = false for all email records (DNS-only; email traffic must not enter the CF proxy).

MX records

Cloudflare Email Routing MX records are published by CF when Email Routing is enabled on the zone. The canonical set (as of CF documentation) is:

Priority MX value
10 route1.mx.cloudflare.net
20 route2.mx.cloudflare.net
30 route3.mx.cloudflare.net

Resolved (cloudflare-expert R1): Neither modules/cloudflare-dns/ nor modules/cloudflare-email-routing/ creates the MX records in Tofu state. Cloudflare's Enable Email Routing API endpoint auto-creates and locks the MX records (amir/linda/isaac.mx.cloudflare.net) when cloudflare_email_routing_settings is applied. The DNS module's for_each map must not contain MX entries — duplication causes apply errors. Same goes for the CF auto-added SPF (include:_spf.mx.cloudflare.net); see §SPF merge below for the import procedure that consolidates the CF auto-SPF with OCI's send-host include into a single TXT record.

SPF TXT record

Name Type Value
marnissi-holdings.com TXT v=spf1 include:_spf.mx.cloudflare.net include:eu-milan-1.rp.oracleemaildelivery.com ~all
  • include:_spf.mx.cloudflare.net authorises Cloudflare Email Routing for inbound-relayed outbound mail (if any relay-send path is used).
  • include:eu-milan-1.rp.oracleemaildelivery.com authorises OCI Email Delivery send hosts in eu-milan-1 (region-specific form; oci-expert verified in ADR-0005).
  • ~all (softfail) is the initial posture. Harden to -all (hardfail) only after DMARC reaches p=reject (Day 60+ per DMARC progression below) and aggregate reports show no legitimate sources outside the two includes.

DMARC TXT record

Name Type Value
_dmarc.marnissi-holdings.com TXT v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc-reports@marnissi-holdings.com; aspf=s; adkim=s

DMARC progression (per ADR-0005 §DNS record handoff, committed here as the owning module):

Timeline Policy
Day 0 (initial apply) p=quarantine; pct=25; rua=mailto:dmarc-reports@marnissi-holdings.com; aspf=s; adkim=s
Day 30 (after reviewing aggregate reports) p=quarantine; pct=100
Day 60 (if reports show clean) p=reject; pct=100

The rua address dmarc-reports@marnissi-holdings.com routes via CF Email Routing (ADR-0009) to marnissi.investments@gmail.com. aspf=s (strict SPF alignment) and adkim=s (strict DKIM alignment) are set from day one; they do not change across the progression.

Policy escalation is a manual Tofu variable change (var.dmarc_policy and var.dmarc_pct); infra-agent opens a PR for each step after operator sign-off. finops-agent ledger entries track the progression milestones.

DKIM CNAME record (OCI Email Delivery)

Consumed from modules/oci-email/ outputs (ADR-0005 PR4):

Name Type Value CF proxied
module.oci_email.dkim_cname_host CNAME module.oci_email.dkim_cname_value false

The DKIM selector name is mgh-v1 (stable; immutable after publish; rotation procedure in ADR-0005 §Operational surface).

OCI Email Delivery domain verification TXT record

OCI requires a TXT record at the zone apex to verify domain ownership before DKIM state transitions from CREATING to ACTIVE. The TXT value is generated by OCI post-apply and is not predictable from Tofu plan output.

Recommendation (cloudflare-expert verified): This record is ephemeral — required once for verification, then safe to remove. It is declared as a Tofu cloudflare_record resource with value = var.oci_domain_verification_txt. The variable is left empty ("") at plan time and populated manually by the operator from the OCI console after tofu apply of modules/oci-email/, then a second tofu apply of modules/cloudflare-dns/ publishes it. The procedure is documented in infra/scripts/bootstrap-oci.md §Step 10 Part A. Alternatively: if OCI offers a data source to read the verification TXT value at plan time, infra-agent should use it to eliminate the manual step — check the oracle/oci provider changelog.

Inter-module dependency chain

modules/oci-email/        (ADR-0005, PR4)
  └── outputs: dkim_cname_host, dkim_cname_value, spf_include
modules/cloudflare-tunnel/ (ADR-0007, PR5 — parallel)
  └── output: tunnel_id
modules/cloudflare-dns/   (ADR-0006, PR5 — after both above)
  └── consumes: tunnel_id, dkim_cname_host, dkim_cname_value

modules/cloudflare-email-routing/ (ADR-0009, future PR)
  └── (MX records owned by Cloudflare, not by either Tofu module)

modules/cloudflare-dns/ must be applied after modules/oci-email/ and modules/cloudflare-tunnel/. In envs/<env>/main.tf, this is enforced by Tofu data references (not depends_on) — the DKIM CNAME record resource references module.oci_email.dkim_cname_value directly, creating an implicit dependency.

Consequences

  • Cost (delta vs free tier): $0. All zone settings, DNSSEC, Single Redirects, and DNS records are free on the Cloudflare Free plan. CF Free plan quotas consumed by this ADR: DNSSEC (free), Single Redirects (unlimited free), DNS records (unlimited free). Page Rules quota (3 total): 0 consumed by this ADR — the www redirect uses Single Redirects as specified.

  • Operational surface:

  • CLOUDFLARE_API_TOKEN env var: scoped to DNS Edit + Page Rules Edit + Email Routing Edit + DNSSEC Edit for marnissi-holdings.com only. Rotation cadence: every 6 months. Runbook entry in infra/scripts/rotate-secrets.md (infra#5). Token value lives in env vars or vault only — never in .tfvars, ADRs, or PR bodies.
  • DMARC aggregate reports arrive at dmarc-reports@marnissi-holdings.com → Gmail. Operator reviews before each DMARC policy escalation step (Day 30, Day 60).
  • DNSSEC DS record must be published at the registrar (Squarespace) after CF DNSSEC is enabled. CF generates the DS record; operator copies it to Squarespace DNS settings. One-time manual step; documented in infra/scripts/bootstrap-oci.md or a new bootstrap-cf.md.
  • OCI domain verification TXT: ephemeral manual step post-modules/oci-email/ apply (see §OCI Email Delivery domain verification TXT record above).
  • HSTS preload submission: after the zone is stable, submit marnissi-holdings.com to https://hstspreload.org. Operator action; one-time.

  • Security posture:

  • DNSSEC on: DS record at registrar + zone signing at CF. Prevents spoofed DNS responses.
  • No A records to OCI public IPs: CNAME-only tunnel topology is the enforcement mechanism for zero public ingress on OCI. secops-agent must check on every infra PR that no A record to an OCI IP is introduced.
  • HSTS with includeSubDomains and preload: prevents protocol downgrade across all current and future subdomains. Operator must not add a subdomain that cannot serve HTTPS before HSTS preload is submitted.
  • Minimum TLS 1.2: enforced at the CF edge.
  • SPF ~all initially; hardens to -all after DMARC reaches p=reject. SPF includes are narrow (two include: clauses only).
  • DMARC aspf=s; adkim=s: strict alignment from day one. No relaxed alignment.
  • secops-agent checklist for every infra PR touching this module: (a) DNSSEC still enabled; (b) no new A record to OCI IP; (c) no TLS mode downgrade below full_strict; (d) HSTS settings unchanged; (e) SPF record has no new include: without a corresponding ADR; (f) DMARC policy has not been weakened.

  • Reversibility: DNS records are portable. If Cloudflare becomes unsuitable as the DNS provider, all records (MX, SPF, DKIM CNAME, DMARC, CNAMEs) can be exported and imported to any authoritative DNS provider. TTLs are CF-managed (default 300s proxied, configurable); cutover takes minutes once NS delegation changes. The only CF-specific coupling in this module is CNAME flattening at the apex (a CF feature) — any DNS provider supporting RFC-violating apex CNAME synthesis (Route 53, Bunny DNS) would support the same topology.

  • Migration path if we revisit: If CF Free tier ceilings become a constraint (today: none consumed beyond this ADR), upgrade to CF Pro or migrate DNS to an equivalent provider. All records in modules/cloudflare-dns/ are provider-agnostic data; only the Tofu provider block changes on migration. Estimated migration effort: 1 sprint to switch provider + re-validate all records.

Alternatives considered

Option Why rejected
AWS Route 53 Adds AWS as a third cloud provider. Costs ~$0.50/month per hosted zone + per-query pricing. No WAF, Tunnel, or Access integration — those would still require CF anyway, making Route 53 purely redundant DNS.
Google Cloud DNS Adds Google Cloud (distinct from Google Workspace) as a billing surface. Same integration gap as Route 53 — no Tunnel or Access.
OCI DNS OCI DNS has no WAF, no Tunnel coupling, and no CDN. MGH already runs CF for edge/security; splitting DNS off to OCI DNS fragments the control plane and loses CF's CNAME flattening, Single Redirects, and DNSSEC UX.
Page Rules for www redirect CF Free plan allows 3 Page Rules. Using one for a permanent redirect wastes a Page Rule on a static concern. Single Redirects are free and unlimited; Page Rule quota is preserved for operational needs (e.g., cache bypass, IP-based rules) that Single Redirects cannot satisfy.
Per-app subdomain delegation to sub-zones Delegating admin.marnissi-holdings.com to a separate NS set would require a sub-zone per app and separate CF zone management. Premature at v1 — single zone is operationally simpler, and CF Access + Tunnel routing is per-hostname, not per-zone. Revisit if MGH needs per-team DNS autonomy.
Hardcode tunnel UUID Using a variable (var.tunnel_id sourced from module.cloudflare_tunnel.tunnel_id) keeps the DNS module decoupled from the specific tunnel credential. Hardcoding would require a DNS module PR every time the tunnel is rotated.