Skip to content

Cloudflare DNS & CDN

All public traffic to this platform flows through Cloudflare. Cloudflare serves as the DNS provider, CDN, DDoS shield, and edge TLS terminator for every domain. DNS records and cache rules are managed entirely through Terraform.

CapabilityHow It’s Used
DNSAuthoritative nameserver for all five domain zones
CDN cachingStatic assets cached at 300+ edge locations worldwide
DDoS protectionAlways-on L3/L4/L7 mitigation on the free plan
TLS at the edgeCloudflare-issued certificates presented to end users
Serve staleCached content served when the origin is down
IP maskingOrigin server IP hidden behind Cloudflare proxy

All of this runs on Cloudflare’s free plan — no paid features are required.

graph LR
    user["User Browser"]
    cf["Cloudflare Edge<br/>(nearest PoP)"]
    origin["Azure Public IP<br/>(node1)"]
    traefik["Traefik<br/>(K3s)"]

    user -->|"1. DNS lookup<br/>returns Cloudflare IP"| cf
    cf -->|"2. TLS terminated<br/>at edge"| cf
    cf -->|"3. Proxied to origin<br/>over HTTPS"| origin
    origin --> traefik
  1. The user’s DNS lookup returns a Cloudflare anycast IP (not the origin server IP)
  2. Cloudflare terminates TLS using its own managed certificate
  3. If the response is cached, Cloudflare serves it directly from the edge — the request never reaches the origin
  4. For cache misses, Cloudflare proxies the request to the origin over HTTPS (Full SSL mode)
  5. Traefik receives the request and routes to the correct service

Five separate Cloudflare zones are managed, each with its own zone ID:

ZoneDomainSubdomainsTerraform Reference
kevinryan.iokevinryan.iowww, brand, docs, analytics, monitoringmodule.cloudflare + root records
aiimmigrants.comaiimmigrants.comwwwmodule.cloudflare_aiimmigrants
specmcp.aispecmcp.aiwwwmodule.cloudflare_specmcp
sddbook.comsddbook.comwwwmodule.cloudflare_sddbook
distributedequity.orgdistributedequity.orgwwwmodule.cloudflare_distributedequity

The Cloudflare Terraform module creates DNS records for each domain zone. Every zone gets:

RecordTypeNameContentProxied
RootA@node1 public IPYes (orange cloud)
WWWAwwwnode1 public IPYes (orange cloud)
SubdomainsA<subdomain>node1 public IPYes (orange cloud)

All records are proxied (orange cloud enabled), meaning:

  • DNS lookups return Cloudflare’s anycast IP, not the origin
  • Traffic is routed through Cloudflare’s network
  • The origin server’s real IP is never exposed
  • CDN caching and DDoS protection are active

The TTL is set to 1 (automatic) for all proxied records, as Cloudflare manages caching independently of DNS TTL.

The kevinryan.io zone has the most records since it hosts both site subdomains and platform service subdomains:

RecordHostnameManaged By
@kevinryan.ioCloudflare module
wwwwww.kevinryan.ioCloudflare module
brandbrand.kevinryan.ioCloudflare module (subdomain list)
docsdocs.kevinryan.ioCloudflare module (subdomain list)
analyticsanalytics.kevinryan.ioRoot module (standalone record)
monitoringmonitoring.kevinryan.ioRoot module (standalone record)

The analytics and monitoring records are managed directly in the root Terraform module rather than via the Cloudflare module, since they are platform service subdomains rather than site subdomains:

resource "cloudflare_record" "analytics" {
zone_id = var.cloudflare_zone_id
name = "analytics"
content = module.network.public_ip_address
type = "A"
proxied = true
ttl = 1
}
resource "cloudflare_record" "monitoring" {
zone_id = var.cloudflare_zone_id
name = "monitoring"
content = module.network.public_ip_address
type = "A"
proxied = true
ttl = 1
}

Every hostname in the platform and what it resolves to:

HostnameSite/Service
kevinryan.ioPortfolio
www.kevinryan.ioPortfolio
brand.kevinryan.ioBrand Guidelines
docs.kevinryan.ioDocumentation
analytics.kevinryan.ioUmami Analytics
monitoring.kevinryan.ioGrafana
aiimmigrants.comAI Immigrants
www.aiimmigrants.comAI Immigrants
specmcp.aiSpecMCP
www.specmcp.aiSpecMCP
sddbook.comSDD Book
www.sddbook.comSDD Book
distributedequity.orgDistributed Equity
www.distributedequity.orgDistributed Equity

All 14 hostnames resolve to Cloudflare’s edge, which proxies to the same origin (node1’s public IP). Traefik handles host-based routing at the cluster level.

Cloudflare’s SSL/TLS mode is set to Full (configured in the Cloudflare dashboard). This means:

SegmentEncryptionCertificate
User → CloudflareHTTPSCloudflare Universal SSL (trusted, auto-renewed)
Cloudflare → OriginHTTPSTraefik default cert (self-signed acceptable)
graph LR
    user["User"] -->|"Trusted TLS<br/>(Cloudflare cert)"| cf["Cloudflare"]
    cf -->|"TLS<br/>(any cert accepted)"| origin["Traefik"]
    origin -->|"Plain HTTP"| svc["Service Pod"]

Full mode (not Full Strict) is used because Traefik serves a self-signed certificate by default. Full mode encrypts traffic between Cloudflare and the origin but does not require a CA-signed certificate on the origin. This provides encryption in transit without the overhead of managing Let’s Encrypt certificates on the cluster.

Note: SSL mode is currently configured manually in the Cloudflare dashboard rather than via Terraform. The API token lacks Zone Settings:Edit permission, and the relevant Terraform resource has known issues with read-only settings.

Each domain zone has a Terraform-managed cache ruleset that overrides origin cache headers:

resource "cloudflare_ruleset" "cache" {
zone_id = var.zone_id
name = "Cache rules for ${var.domain}"
description = "Cache static site and serve stale on origin failure"
kind = "zone"
phase = "http_request_cache_settings"
rules {
action = "set_cache_settings"
action_parameters {
cache = true
edge_ttl {
mode = "override_origin"
default = 86400
}
serve_stale {
disable_stale_while_updating = false
}
}
expression = local.cache_expression
description = "Serve stale content on origin failure"
enabled = true
}
}
SettingValueEffect
cachetrueEnable caching for all matching requests
edge_ttl86,400s (24 hours)Cache content at the edge for 24 hours, overriding any Cache-Control headers from the origin
serve_staleEnabledServe cached (potentially stale) content if the origin is unreachable
disable_stale_while_updatingfalseContinue serving stale content while fetching a fresh copy in the background

The cache rule applies to all hostnames in the zone. The expression is dynamically constructed from the domain and its subdomains:

(http.host eq "kevinryan.io") or (http.host eq "www.kevinryan.io") or (http.host eq "brand.kevinryan.io") or (http.host eq "docs.kevinryan.io")

For zones without subdomains (e.g. aiimmigrants.com), the expression covers just the root and www:

(http.host eq "aiimmigrants.com") or (http.host eq "www.aiimmigrants.com")

Serve Stale: Resilience Without Redundancy

Section titled “Serve Stale: Resilience Without Redundancy”

The serve_stale setting is particularly important for this platform. With a single-origin architecture (one K3s cluster, one public IP), there is no automatic failover. If the origin goes down:

  • Without serve stale: All requests fail immediately
  • With serve stale: Cloudflare continues serving the last cached version of every page for the duration of the edge TTL

For static sites that change infrequently, this provides effective resilience without the cost of a redundant origin. Visitors experience no disruption as long as the content they request was cached within the last 24 hours.

All proxied records benefit from Cloudflare’s DDoS mitigation:

  • L3/L4 protection — volumetric and protocol attacks are absorbed by Cloudflare’s network before reaching the origin
  • L7 protection — HTTP flood protection filters malicious requests at the application layer
  • Always on — no configuration needed, included in the free plan
  • IP masking — the origin IP is never exposed in DNS responses, preventing direct-to-origin attacks

This is especially relevant for a single-origin platform where the origin has no redundancy. Cloudflare acts as a shield that absorbs attack traffic at the edge.

Beyond DDoS protection, proxying through Cloudflare provides:

BenefitHow
Hidden origin IPDNS returns Cloudflare IPs, not the Azure VM’s public IP
Bot mitigationBasic bot management on the free plan filters known bad actors
Browser integrity checkChallenges requests with suspicious characteristics
Hotlink protectionPrevents other sites from embedding your assets

The Cloudflare module (infra/modules/cloudflare/) is called once per domain zone:

module "cloudflare" {
source = "./modules/cloudflare"
zone_id = var.cloudflare_zone_id
vm_public_ip = module.network.public_ip_address
domain = "kevinryan.io"
subdomains = ["brand", "docs"]
}
module "cloudflare_aiimmigrants" {
source = "./modules/cloudflare"
zone_id = var.cloudflare_zone_id_aiimmigrants
vm_public_ip = module.network.public_ip_address
domain = "aiimmigrants.com"
}

Each invocation creates:

  • Root A record (@)
  • www A record
  • One A record per entry in subdomains
  • A cache ruleset covering all hostnames in the zone
VariableDescription
zone_idCloudflare zone ID (one per domain)
vm_public_ipOrigin server IP (node1’s Azure public IP)
domainDomain name (used in cache rule expressions)
subdomainsList of additional subdomain records (default: [])

To add a new domain to the platform:

  1. Register the domain and set Cloudflare as the authoritative nameserver
  2. Add the zone ID as a new Terraform variable in infra/variables.tf
  3. Add a new Cloudflare module call in infra/main.tf:
module "cloudflare_newsite" {
source = "./modules/cloudflare"
zone_id = var.cloudflare_zone_id_newsite
vm_public_ip = module.network.public_ip_address
domain = "newsite.com"
}
  1. Pass the zone ID value via terraform.tfvars or as a GitHub Actions secret
  2. Run terraform plan and terraform apply (via the Terraform workflow)

The module handles root, www, and cache rule creation automatically. For additional subdomains, add them to the subdomains list.

For subdomains on kevinryan.io (e.g. a new api.kevinryan.io):

  • Site subdomains — add the subdomain to the subdomains list in the existing module.cloudflare call
  • Service subdomains — add a standalone cloudflare_record resource in the root module (as done for analytics and monitoring)