writing
Hardening a Static Site on Cloudflare Pages
A static site is often treated as low risk. There is no server process, no database connection, and no application logic to exploit. That framing is mostly correct, but it misses the layer that browsers actually interact with: the HTTP response headers. Those headers control what the browser is allowed to do, what it may load, and how the page behaves when something goes wrong. A static site with default headers leaves a surprising amount of behavior unconstrained.
This article walks through the security headers I added to an Astro site hosted on Cloudflare Pages, and why I applied them at the edge rather than as static files.
Why edge rules instead of static files
Astro generates a fully static output directory. It is tempting to drop a _headers file into the build output and let the platform append those headers to every response. That works, but it has a drawback: the header set is baked into the deployment. If a header needs a policy change, you rebuild and redeploy just to adjust a couple of lines.
Cloudflare Pages supports applying headers through edge transform rules instead. These rules live at the Cloudflare zone level and apply to requests before the content reaches the browser. The advantage is operational: you can adjust a security policy in the dashboard and have it take effect immediately, without touching the site build or triggering a redeploy.
The header set
The configuration uses several headers, each addressing a different class of browser behavior.
HSTS
Strict-Transport-Security: max-age=31536000; includeSubDomains
This tells the browser to only ever contact the site over HTTPS for the next year, including subdomains. It prevents downgrade attacks where an attacker forces a connection back to plain HTTP. The value is deliberately long because HSTS is meant to be sticky; a short value creates a window where the policy silently expires.
MIME sniffing
X-Content-Type-Options: nosniff
Browsers sometimes try to infer a file type from its content even when the server declares one. If an attacker can upload content that gets interpreted as a different type, that can lead to script execution. This header disables that inference and forces the browser to respect the declared content type.
Clickjacking
X-Frame-Options: DENY
A site that can be embedded in an iframe on another domain is vulnerable to clickjacking: an attacker overlays invisible UI on top of your page and tricks users into clicking things they did not intend. Blocking framing entirely is the safest policy for a site that does not need to be embedded anywhere.
Referrer data
Referrer-Policy: strict-origin-when-cross-origin
This limits how much of the URL is leaked in the Referer header when a user clicks a link to another site. For same-origin requests the full URL is sent, but for cross-origin requests only the origin is sent. That keeps sensitive path information off external servers.
Permissions
Permissions-Policy: geolocation=(), microphone=(), camera=()
Browsers enable certain powerful APIs by default even when the page never uses them. This header explicitly disables geolocation, microphone, and camera access at the page level. It is a defense-in-depth measure: even if a script somehow runs, it cannot silently request those capabilities.
Applying the rules
The key detail is the phase. Cloudflare applies HTTP response header transforms in a dedicated phase that runs after the origin or static asset is selected but before the response is returned to the client. Creating a rule there lets you match paths and attach the header set without modifying the site itself.
The rules are scoped to the production host. For this particular setup they cover the main site domain, which is enough because the site does not serve untrusted user content and has no third-party embeds that would require relaxing the policy.
Results and trade-offs
After deployment, a quick check of the response headers confirms every policy is live. HSTS, nosniff, frame denial, referrer restriction, and permissions restrictions all appear on the response.
The trade-offs are worth noting. HSTS with includeSubDomains means every subdomain under the zone must also support HTTPS, otherwise those subdomains become unreachable. The permissions policy is intentionally strict; if the site later adds an embedded map or camera feature, that header will need a deliberate update. The frame denial likewise rules out embedding the site in any third-party dashboard.
None of these are heavy trade-offs for a personal static site, which is why I consider the whole set a strictness win. Hosting on Cloudflare Pages means the deployment itself stays simple, while the security posture benefits from policy that lives at the edge and can be adjusted anytime.