Back to Blog

Hardening this Site: Cloudflare + Astro

4 min read

This site runs on Astro deployed to Cloudflare Pages. No servers to patch. No databases to secure. No runtime vulnerabilities to exploit. Here’s how I hardened it—and why static sites are inherently more secure.

Why Static = Secure

Traditional web applications have attack surface everywhere:

  • Server-side code: SQL injection, command injection, SSRF
  • Runtime dependencies: Vulnerable libraries, outdated frameworks
  • Database layer: Data breaches, privilege escalation
  • State management: Session hijacking, CSRF

Static sites eliminate most of this by design. There’s no server-side code to exploit. No database to breach. Every page is pre-rendered HTML served from edge CDN nodes.

The remaining attack surface:

  • Client-side JavaScript (minimal)
  • CDN/hosting provider (Cloudflare)
  • Build pipeline (GitHub Actions)
  • DNS infrastructure

Security Headers Configuration

Every response from this site includes hardened security headers. Here’s the configuration in public/_headers:

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Resource-Policy: same-origin

Let’s break these down:

Content-Security-Policy

CSP is the most powerful header for preventing XSS and data injection attacks.

default-src 'self'

Base policy: only load resources from same origin.

script-src 'self' https://challenges.cloudflare.com

JavaScript only from this domain and Cloudflare’s Turnstile (for contact form CAPTCHA).

style-src 'self' 'unsafe-inline'

Styles from this domain. unsafe-inline required for Astro’s scoped styles—a tradeoff I accept given the controlled environment.

frame-ancestors 'none'

Prevent this site from being embedded in frames (clickjacking protection).

HSTS (Strict-Transport-Security)

max-age=31536000; includeSubDomains; preload

Force HTTPS for one year, including all subdomains. The preload directive registers the domain in browser HSTS preload lists—even first-time visitors get HTTPS enforcement.

Additional Headers

  • X-Frame-Options: DENY - Redundant clickjacking protection for older browsers
  • X-Content-Type-Options: nosniff - Prevent MIME-type sniffing attacks
  • Referrer-Policy: strict-origin-when-cross-origin - Limit referrer information leakage
  • Permissions-Policy - Disable unused browser features (camera, mic, geolocation)
  • COOP/CORP - Isolate browsing context, prevent Spectre-like attacks

Cloudflare WAF Configuration

Beyond headers, Cloudflare provides edge security:

Managed Rules

Enable Cloudflare Managed Ruleset with:

  • OWASP Core Ruleset
  • Cloudflare Specials (proprietary threat intelligence)
  • Anomaly scoring for suspicious patterns

Custom Rules

Block obvious attack patterns:

(http.request.uri.query contains "SELECT" and http.request.uri.query contains "FROM") or
(http.request.uri.query contains "UNION" and http.request.uri.query contains "SELECT") or
(http.request.uri.query contains "<script") or
(http.request.uri.query contains "javascript:")

Rate Limiting

Protect against brute force and DDoS:

Rule: Contact Form Protection
If: URI Path equals "/contact" AND Request Method equals "POST"
Then: Rate limit to 5 requests per minute per IP
Action: Challenge

Bot Management

Configure bot scores:

  • Definitely automated (score 1-29): Block
  • Likely automated (score 30-49): Challenge
  • Likely human (score 50+): Allow

Build Pipeline Security

The site builds via GitHub Actions. Key security measures:

Dependency Pinning

# astro.config.mjs - Pin exact versions
{
  "dependencies": {
    "astro": "5.0.0",  # Not ^5.0.0
    "@astrojs/sitemap": "3.0.0"
  }
}

Minimal Permissions

permissions:
  contents: read
  pages: write
  id-token: write

Dependency Review

- name: Dependency Review
  uses: actions/dependency-review-action@v3
  with:
    fail-on-severity: high

DNS Security

DNSSEC

Enabled via Cloudflare. Prevents DNS spoofing and cache poisoning by cryptographically signing DNS records.

CAA Records

Certificate Authority Authorization restricts which CAs can issue certificates:

virtualpavi.com. CAA 0 issue "digicert.com"
virtualpavi.com. CAA 0 issue "letsencrypt.org"
virtualpavi.com. CAA 0 iodef "mailto:security@virtualpavi.com"

Security.txt

Following RFC 9116, I publish security contact information:

# public/.well-known/security.txt
Contact: mailto:security@virtualpavi.com
Expires: 2027-12-31T23:59:00.000Z
Preferred-Languages: en
Canonical: https://virtualpavi.com/.well-known/security.txt

This provides a standardized way for security researchers to report vulnerabilities.

Monitoring and Response

Cloudflare Analytics

Monitor for:

  • Unusual traffic spikes (potential DDoS)
  • High error rates (potential attacks)
  • Geographic anomalies (targeted attacks)

Security Headers Validation

Regular checks via:

  • securityheaders.com
  • Mozilla Observatory
  • CSP Evaluator

Subresource Integrity

For any external resources (none currently), implement SRI:

<script 
  src="https://example.com/script.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous">
</script>

The Result

This configuration achieves:

  • A+ rating on securityheaders.com
  • A rating on Mozilla Observatory
  • Zero JavaScript beyond minimal interactivity
  • No cookies (privacy by design)
  • No tracking scripts

Static sites aren’t just about performance. They’re about reducing attack surface to its minimum. When there’s nothing to exploit, defense becomes straightforward.


Questions about hardening your own site? Reach out.