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:[email protected]"

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:[email protected]).

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

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)