Notes from production
SSLSecurityCloudflare

Fast, safe SSL & security on the edge

Once the domain is on Cloudflare, the next job is to make it fast and safe by default. The good news: the whole security baseline below is free, and it's layered — every request from the internet is checked at Cloudflare's edge, one layer at a time, before it ever reaches your site. This is Part 4 of the deploy series, and it's all about the security layers.

Think of it as a stack of gates. A request falls down through each one — forced onto HTTPS, encrypted, screened for attacks and bots, and finally handed your security headers — and only a clean request reaches the page.

Request from the internetAlways Use HTTPSTLS — Full (strict)WAF + Bot Fight ModeTurnstileon the contact form onlySecurity headersnosniff · X-Frame-Options · HSTS · CSP
Every request is checked at Cloudflare's edge — layer by layer — before it reaches the site.

Below the last gate sits your site & form. Let's walk down the stack.

HTTPS & TLS — encrypt everything

TLS (Transport Layer Security, the modern name for SSL) is the encryption behind the padlock — it scrambles traffic so nobody between the visitor and the site can read or tamper with it. Three free switches do the heavy lifting:

  • Always Use HTTPS — if anyone reaches http://pangaea.id, Cloudflare redirects them to the https:// version. The insecure door is closed; visitors only ever travel encrypted.
  • TLS mode → Full (strict) — this controls how Cloudflare talks to your server (the origin). "Full (strict)" encrypts both legs — browser↔edge and edge↔origin — and checks that the origin's certificate is real and trusted. The weaker modes ("Flexible", "Full") leave the second leg unencrypted or unverified; don't use them.
  • Minimum TLS 1.2, and enable TLS 1.3 — refuse the old, broken versions of the protocol and prefer the newest, fastest one.

HSTS — "only ever reach me over HTTPS"

HSTS (HTTP Strict Transport Security) is a header that tells the browser one thing: only ever reach me over HTTPS, even if someone types http:// or clicks an old link. The browser remembers it and upgrades every future request automatically — there's no insecure first hop to intercept.

It's a single header on the /* block:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

max-age=31536000 is one year (in seconds); includeSubDomains extends the rule to every subdomain; preload opts you into the browser-built-in list.

CAA records — protecting your certificate's auto-renewal

This is the layer most people have never heard of, and it's the one that quietly breaks padlocks.

A CAA record (Certification Authority Authorization) is a small DNS record that lists which Certificate Authorities are allowed to issue an HTTPS certificate for your domain. A Certificate Authority (CA) is one of the trusted companies that hand out HTTPS certs — Let's Encrypt, Google Trust Services, DigiCert, and so on. The CAA record is a whitelist: if a CA isn't on it, browsers won't honour a cert it issues, and a well-behaved CA won't even try.

Why it matters

Cloudflare auto-renews the Universal SSL certificate for your site roughly every 90 days — silently, in the background. Here's the trap: if a CAA record exists but omits a CA that Cloudflare uses, the next auto-renewal fails. You won't notice on day one. Then the old cert expires, and suddenly every visitor sees a broken padlock and a scary "Not secure" warning. A CAA record you added to be safe becomes the thing that takes the site down.

The safe play

Here's the live CAA set for pangaea.id (the letsencrypt.org lines are ours; the rest were backfilled by Cloudflare):

@   CAA   0 issue     "letsencrypt.org"
@   CAA   0 issuewild "letsencrypt.org"
@   CAA   0 issue     "pki.goog; cansignhttpexchanges=yes"
@   CAA   0 issuewild "pki.goog; cansignhttpexchanges=yes"
@   CAA   0 issue     "ssl.com"
@   CAA   0 issuewild "ssl.com"
@   CAA   0 issue     "sectigo.com"
@   CAA   0 issuewild "sectigo.com"
@   CAA   0 issue     "comodoca.com"
@   CAA   0 issuewild "comodoca.com"
@   CAA   0 issue     "digicert.com; cansignhttpexchanges=yes"
@   CAA   0 issuewild "digicert.com; cansignhttpexchanges=yes"
@   CAA   0 iodef     "mailto:sales@pangaea.id"

Reading the columns:

  • @ — the apex of the domain (pangaea.id itself).
  • 0 — a flag; 0 means "non-critical" (a CA that doesn't understand the record may still proceed).
  • issue — who may issue normal certificates.
  • issuewild — who may issue wildcard certificates (*.pangaea.id). This one is required: Cloudflare's edge certificate is a wildcard, so a CAA set with no issuewild would block the renewal.
  • iodef — where a CA emails a report if someone requests a cert the policy forbids (mailto:sales@pangaea.id).

Verify it

Ask DNS directly what CAA records exist:

dig CAA pangaea.id +short

And confirm who actually issued the live certificate:

openssl s_client -connect www.pangaea.id:443 -servername www.pangaea.id </dev/null 2>/dev/null | openssl x509 -noout -issuer

Status on pangaea.id: configured and verified. The live certificate is issued by Google Trust Services, and renewal is protected because Cloudflare's CAs are auto-authorised in the CAA set above.

Security headers — instructions on every response

Headers are short instructions Cloudflare attaches to every response, telling the browser how to behave safely. Four cheap ones close common holes:

  • X-Content-Type-Options: nosniff — stop the browser from guessing a file's type (which can turn an uploaded image into an executable script). It must trust the declared type only.
  • X-Frame-Options: DENY — forbid anyone from embedding the site inside an <iframe>, which defeats "clickjacking" (a hidden frame tricking users into clicking).
  • Referrer-Policy: strict-origin-when-cross-origin — when a visitor clicks out to another site, don't leak the full URL they came from; share only the bare origin.
  • Permissions-Policy: geolocation=(), microphone=(), camera=() — switch off browser features the site never uses, so no script can prompt for location, mic, or camera.

CSP — the script allowlist

The big one is CSP (Content-Security-Policy): an allowlist of where scripts, styles, fonts and images may load from. Anything not on the list is refused — so an injected or malicious resource simply never runs. This is the strongest single defence against cross-site scripting. Here's the live policy:

default-src 'self'; script-src 'self' {{INLINE_SCRIPT_HASHES}} https://static.cloudflareinsights.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: https://www.googletagmanager.com https://*.google-analytics.com; connect-src 'self' https://cloudflareinsights.com https://api.web3forms.com https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests

In plain terms, a few of the key directives:

  • default-src 'self' — by default, only load things from our own origin. Every other directive narrows from there.
  • font-src 'self' — fonts load only from our own server. The site self-hosts its fonts, so there's no font CDN to allow — the allowlist stays tight.
  • connect-src … https://api.web3forms.com … — the contact form posts to Web3Forms, so that one endpoint is allowed for network requests; everything else is blocked.
  • frame-ancestors 'none' — nobody may frame the site (the modern partner to X-Frame-Options).
  • object-src 'none' — no Flash/plugin objects, ever.
  • upgrade-insecure-requests — quietly rewrite any stray http:// sub-resource to https://.

The interesting part is script-src. There is no 'unsafe-inline' for scripts — which is what would normally let any inline <script> run. The site's only inline scripts (the vite-react-ssg bootstrap and the GA4 gtag snippet) are instead pinned by a per-build SHA-256 hash. A build step, apps/labs/scripts/csp-hash.mjs, hashes those exact scripts and writes the hashes into _headers, replacing the {{INLINE_SCRIPT_HASHES}} placeholder. So only those exact scripts run — change a single character and the hash no longer matches and the script is refused. The allowlist can never silently drift.

Anti-bot & firewall, in plain terms

The last three switches keep automated abuse off the site, all free:

  • Bot Fight Mode — a one-click switch that challenges obvious bots (scrapers, brute-forcers) before they reach your pages. It already exempts verified search and AI crawlers, so Google, Bing and the answer-engine bots still index the site normally.
  • WAF (Web Application Firewall) — Cloudflare's free managed ruleset blocks known attack patterns (SQL injection, common exploit probes) at the edge, before they touch your app. It's a baseline you just turn on.
  • Turnstile — Cloudflare's free, privacy-friendly "are you human?" check — the modern, no-puzzle replacement for CAPTCHA. Put it on the contact form only, never the whole site, so real visitors browsing pages never see a challenge — only someone submitting the form is verified.

Do

  • Set TLS → Full (strict) and Always Use HTTPS so every leg is encrypted and the insecure door is closed
  • Add one CA to CAA and let Cloudflare backfill its own — renewals stay safe
  • Scope Turnstile to the contact form, so normal browsing is friction-free

Don't

  • Use TLS "Flexible" or "Full" (the second leg ends up unencrypted or unverified)
  • Hand-list every CA in CAA and accidentally omit one Cloudflare uses — the next renewal breaks
  • Leave Rocket Loader on with a strict CSP, or skip checking the DevTools console after deploy

Cheatsheet

Common questions

Who renews the HTTPS padlock, and how often?

Cloudflare does it automatically — you never touch a certificate file. A certificate is valid for about a 90-day window, and Cloudflare renews well before it expires. Our live certificate is currently issued by Google Trust Services.

Walk me through what actually happens at renewal.

Step by step: (1) a certificate is already serving (Google Trust Services, roughly a 90-day window). (2) Cloudflare kicks off the renewal well before expiry. (3) it proves it controls the domain via a DNS or HTTP challenge. (4) the CAA check — the Certificate Authority looks up your CAA records: no CAA means any CA may issue; a CAA that lists this CA means it succeeds; a CAA that omits this CA means it's refused and renewal fails. (5) the new certificate is installed to Cloudflare's edge automatically and the clock resets. (6) the loop repeats forever.

What is CAA, and what's the footgun?

CAA stands for Certification Authority Authorization — a DNS record listing which Certificate Authorities are allowed to issue your HTTPS certificate. The footgun: an incomplete CAA list silently breaks a future renewal weeks later, not today. Everything looks fine until the next renewal cycle comes around and gets refused because the CA in use wasn't on your list — which makes it nasty to diagnose. Our zone authorizes Let's Encrypt, Google, SSL.com, Sectigo, Comodo, and DigiCert (see the table above).

Next

This is Part 4 (security) of the deploy series. The rest of the path: Part 1 · Point the domain at Cloudflare, Part 2 · CI/CD with GitHub Actions → Pages, Part 3 · Root → www redirect, and the caching sibling — HTTP caching on Cloudflare's CDN. The plain-English story of why we layered it this way is in our build diary: Make it fast and safe.

Sources

  1. Cloudflare — SSL/TLS encryption mode: Full (strict)
  2. Cloudflare — CAA records and certificate issuance
  3. MDN — Content Security Policy (CSP)