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 OpenTofu.

CapabilityHow It’s Used
DNSAuthoritative nameserver for all three 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

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

ZoneDomainSubdomainsOpenTofu Reference
kevinryan.iokevinryan.iowww, brand, docs, hq, analytics, monitoring, dammodule.cloudflare + root records
aiimmigrants.comaiimmigrants.comwwwmodule.cloudflare_aiimmigrants
distributedequity.orgdistributedequity.orgwwwmodule.cloudflare_distributedequity
ai-native-engineer.ioai-native-engineer.iowwwmodule.cloudflare_ainativeengineer

The Cloudflare OpenTofu 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 OpenTofu 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
distributedequity.orgDistributed Equity
www.distributedequity.orgDistributed Equity

All 10 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 OpenTofu. The API token lacks Zone Settings:Edit permission, and the relevant OpenTofu resource has known issues with read-only settings.

Each domain zone has an OpenTofu-managed cache ruleset that decides caching by path class, not by origin headers:

resource "cloudflare_ruleset" "cache" {
zone_id = var.zone_id
name = "Cache rules for ${var.domain}"
description = "Cache immutable assets and serve stale on origin failure; bypass cache for HTML and dynamic app hosts"
kind = "zone"
phase = "http_request_cache_settings"
# dynamic app hosts (e.g. hq) — omitted entirely for zones without them
# ... whole-host cache bypass ...
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 = "Cache immutable assets"
enabled = true
}
rules {
action = "set_cache_settings"
action_parameters {
cache = false
}
expression = local.bypass_static_expression
description = "Bypass cache for HTML and unhashed paths"
enabled = true
}
}

Three mutually exclusive rule classes:

RuleMatchesSettingEffect
Dynamic app hostswhole host (e.g. hq.kevinryan.io), every pathcache = falseNothing on the host is edge-cached — LibreChat HTML/API/auth and overlay deploys are immediately visible
Immutable assetsstatic-site hosts AND asset paths (/_next/static/, /_astro/, or ends with .svg .png .ico .pdf .woff2 .woff .jpg .jpeg .webp)cache = true, edge TTL 24h, serve staleContent-addressed assets cached at the edge and served stale while the origin is down
HTML + unhashed pathsstatic-site hosts AND NOT asset pathscache = falseHTML and unhashed assets (brand.css) never edge-cached — deploys are immediately visible

The static-site rules’ expressions are mutually exclusive by construction (not <asset paths>), so rule order is irrelevant; the dynamic-host rule is host-disjoint from them.

The ruleset originally used edge_ttl mode = "override_origin", default = 86400 on every path, which edge-cached HTML for up to 24 hours despite deploys — the KRA-17 root cause (observed on docs.kevinryan.io as cf-cache-status: HIT with hours-old content).

A naive “fix” of respect_origin does not work either: nginx never sends Cache-Control on browsed HTML paths — try_files $uri $uri.html $uri/index.html serves the file inside location /, which sets no header (only literal .html URIs hit the ~* \.html$ location and get no-cache). Verified on the live docs pod: / and /cloudflare/ return no Cache-Control at all. With respect_origin and no origin directive, Cloudflare caches HTML at its default ~2h edge TTL.

So the edge classifies requests by path instead:

  • HTML and unhashed assets → never edge-cached; a deploy is visible the moment the new pod serves it
  • Hashed/immutable assets → cached 24h at the edge, still served stale while the origin is unreachable

(Browsers still heuristically cache HTML that has no Cache-Control — fixing nginx’s headers is the separate follow-up item on unhashed/HTML asset headers.)

Example for the kevinryan.io zone (static hosts exclude hq, the bypass subdomain):

# immutable assets rule — (static hosts) and (asset paths)
(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") and (starts_with(http.request.uri.path, "/_next/static/") or starts_with(http.request.uri.path, "/_astro/") or ends_with(http.request.uri.path, ".svg") or … )
# HTML + unhashed paths rule — (static hosts) and not (asset paths)
(http.host eq "kevinryan.io" or … ) and not (starts_with(http.request.uri.path, "/_next/static/") or … )

For zones without subdomains (e.g. aiimmigrants.com), the static-host list is just the root and www; zones without cache_bypass_subdomains (all but kevinryan.io) have no dynamic-host rule at all.

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 cached asset (immutable CSS/JS/fonts/images) while the origin is unreachable

HTML is never edge-cached (deliberately — fresh deploys over edge-served HTML during outages, accepted with KRA-17), so serve stale applies to assets only; a page whose cached assets are on the edge still renders, but HTML requests fail while the origin is down. A manual zone purge (purge_everything via the Cloudflare API, CLOUDFLARE_API_TOKEN from .env.agents) remains an emergency tool for clearing bad cached assets.

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 OpenTofu 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 tofu plan and tofu apply (via the OpenTofu 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)