Solanasis Website — Security Hardening Guide

What this is: Post-deployment security checklist and configuration reference. Covers both code-level hardening (handled by Claude Code) and infrastructure hardening (requires dashboard access).

Companion docs:

  • Human setup guide: 01-human-setup-guide.md
  • Build plan: 02-claude-build-plan.md
  • Security hardening plan (code changes): solanasis-site/SECURITY-HARDENING-PLAN.md
  • Credentials playbook: solanasis-credentials-security-playbook.md

Last updated: 2026-03-08


What’s Already Done (Code-Level — Automated)

These are implemented in the codebase and deployed automatically with every push. No manual action needed.

ProtectionImplementationFiles
HSTSStrict-Transport-Security: max-age=31536000; includeSubDomainspublic/_headers
ClickjackingX-Frame-Options: DENYpublic/_headers
MIME sniffingX-Content-Type-Options: nosniffpublic/_headers
Referrer leakageReferrer-Policy: strict-origin-when-cross-originpublic/_headers
Browser API lockdownPermissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()public/_headers
Content Security PolicyRestricts script/style/connect/frame sources to known originspublic/_headers
CORS (API routes)Server-side Access-Control-Allow-Origin: https://solanasis.comsrc/lib/api-helpers.ts
Input length limitsName: 200, email: 254, company: 200, phone: 30, message: 5000 charssrc/lib/api-helpers.ts
Email validationRFC-aligned regex (rejects malformed addresses)src/lib/api-helpers.ts
CAPTCHACloudflare Turnstile on contact form + newslettersrc/components/ContactForm.astro, NewsletterSignup.astro
HoneypotHidden _url field catches botssrc/components/ContactForm.astro, src/pages/api/send-email.ts
HTML escapingAll user input escaped before email templatessrc/lib/api-helpers.ts
Contact obfuscationEmail/phone assembled via JS on hover (not in HTML source)src/components/Footer.astro
Secret separationAPI keys in Cloudflare Pages secrets, not in codewrangler.toml comments
CI/CD pinningAll GitHub Actions pinned to commit SHAs.github/workflows/*.yml
TLSTLS 1.3 with AES-256-GCM (Cloudflare-managed)Automatic
HTTP→HTTPS301 redirect (Cloudflare-managed)Automatic
E2E tests77 tests including security header validationtests/e2e/security-headers.spec.ts

Manual Hardening Checklist (Dashboard Access Required)

These steps require human access to Cloudflare Dashboard and/or Brevo Dashboard. They cannot be automated via code.

1. Cloudflare WAF Rate Limiting (HIGH PRIORITY)

Protects API endpoints from abuse and Brevo quota exhaustion.

  • Go to Cloudflare Dashboardsolanasis.comSecurityWAF
  • Click Rate limiting rulesCreate rule
  • Configure:
    • Rule name: API rate limit
    • When incoming requests match: URI Path contains /api/
    • Rate: 10 requests per 1 minute
    • Counting expression: Same as rule expression
    • Mitigation: Block for 60 seconds
  • Click Deploy
  • Verify: submit the contact form rapidly 11+ times — the 11th should be blocked

2. DKIM for Brevo Email (HIGH PRIORITY)

Without DKIM, emails from hi@solanasis.com (auto-replies, contact confirmations) may land in spam.

  • Go to BrevoSettingsSenders, Domains & Dedicated IPsDomains tab
  • Click on solanasis.com (or add it if not there)
  • Brevo shows DKIM CNAME records to add — copy them
  • Go to Cloudflare Dashboardsolanasis.comDNSRecords
  • Add each CNAME record Brevo provides (typically mail._domainkey)
  • Go back to Brevo → click Verify (allow 5-30 minutes for DNS propagation)
  • Verify: green checkmark appears next to DKIM in Brevo
  • Current status: nslookup -type=CNAME mail._domainkey.solanasis.com returns empty — DKIM is NOT configured

3. www → Apex Redirect (MEDIUM)

Currently www.solanasis.com serves full content instead of redirecting to solanasis.com. This causes SEO dilution.

  • Go to Cloudflare Dashboardsolanasis.comRulesRedirect Rules
  • Click Create rule
  • Configure:
    • Rule name: www to apex
    • When incoming requests match: Hostname equals www.solanasis.com
    • Then: Dynamic redirect
    • Expression: concat("https://solanasis.com", http.request.uri.path)
    • Status code: 301 (Permanent)
    • Preserve query string: Yes
  • Click Deploy
  • Verify: curl -sI https://www.solanasis.com → should return 301 with Location: https://solanasis.com/

4. CAA DNS Record (MEDIUM)

Restricts which Certificate Authorities can issue certificates for your domain.

  • Go to Cloudflare Dashboardsolanasis.comDNSRecords
  • Add two CAA records:
    • Type: CAA, Name: @, Tag: issue, Value: pki.goog (Google Trust Services — current cert issuer)
    • Type: CAA, Name: @, Tag: issue, Value: letsencrypt.org (Cloudflare’s backup CA)
  • Verify: nslookup -type=CAA solanasis.com → shows both issuers

5. DMARC Escalation (MEDIUM — After DKIM)

Current DMARC policy is p=none (monitoring only, no enforcement). Spoofed emails pass through.

  • Wait until DKIM is verified (step 2 above)
  • Go to Cloudflare Dashboardsolanasis.comDNSRecords
  • Find the TXT record for _dmarc.solanasis.com
  • Edit: change p=none to p=quarantine
  • Save
  • After 2-4 weeks of monitoring (no legitimate emails quarantined), escalate to p=reject
  • Verify: nslookup -type=TXT _dmarc.solanasis.com → shows p=quarantine

6. SPF Hardfail (LOW — After DKIM)

Current SPF ends with ~all (softfail). Changing to -all (hardfail) makes spoofed emails more likely to be rejected.

  • Wait until DKIM is verified (step 2 above)
  • Go to Cloudflare Dashboardsolanasis.comDNSRecords
  • Find the TXT record for solanasis.com that starts with v=spf1
  • Edit: change ~all to -all at the end
  • Save
  • Verify: nslookup -type=TXT solanasis.com → SPF record ends with -all

Current DNS Security Status

Run these commands to verify the current state:

# TLS certificate (should be TLS 1.3, Google Trust Services)
echo | openssl s_client -connect solanasis.com:443 -servername solanasis.com 2>/dev/null | grep "Protocol\|Cipher"
 
# HSTS header
curl -sI https://solanasis.com | grep strict-transport
 
# SPF record (should include spf.brevo.com)
nslookup -type=TXT solanasis.com | grep spf
 
# DMARC record (check enforcement level)
nslookup -type=TXT _dmarc.solanasis.com
 
# DKIM record (should NOT be empty)
nslookup -type=CNAME mail._domainkey.solanasis.com
 
# CAA records (restrict cert issuers)
nslookup -type=CAA solanasis.com
 
# Security headers
curl -sI https://solanasis.com | grep -iE "strict-transport|permissions-policy|x-frame|x-content|referrer|content-security"
 
# API CORS (should NOT be wildcard *)
curl -s -X POST https://solanasis.com/api/send-email -H "Content-Type: application/json" -d '{}' -D - -o /dev/null 2>/dev/null | grep access-control

CSP Reference (What Each Directive Allows)

DirectiveAllowed OriginsWhy
default-src'self'Baseline: only same-origin resources
script-src'self' 'unsafe-inline' + Cloudflare Turnstile + Cal.com + UmamiInline scripts needed for Astro build output + 3rd-party integrations
style-src'self' 'unsafe-inline'Inline styles needed for Cal.com modal Shadow DOM
img-src'self' data:Self-hosted images + data URIs for small inline images
connect-src'self' + Turnstile + Cal.com API + Umami API gatewayAPI calls to our server + 3rd-party verification/analytics
frame-srcTurnstile + Cal.comCAPTCHA widget + booking modal iframes

Accepted risks:

  • unsafe-inline in script-src/style-src is required for Astro’s build output and Cal.com’s Shadow DOM styling. Nonce-based CSP would require Astro middleware (tracked in FUTURE-SUGGESTIONS.md).
  • No SRI (Subresource Integrity) on third-party scripts because Cal.com and Turnstile update their scripts frequently, which would break integrity hashes.

Secrets Management

SecretWhere StoredHow Accessed
BREVO_API_KEYCloudflare Pages secretslocals.runtime.env at request time
TURNSTILE_SECRET_KEYCloudflare Pages secretslocals.runtime.env at request time
BREVO_LIST_IDCloudflare Pages secretslocals.runtime.env at request time
CLOUDFLARE_API_TOKENGitHub repo secretsCI/CD deploy workflow only
CLOUDFLARE_ACCOUNT_IDGitHub repo secretsCI/CD deploy workflow only

Setting a Cloudflare Pages secret:

npx wrangler pages secret put SECRET_NAME --project-name solanasis-site

Setting a GitHub repo secret:

gh secret set SECRET_NAME --body "value"

Never put secrets in:

  • wrangler.toml (committed to git)
  • .env in git (use .gitignore)
  • Chat prompts or terminal history

Incident Response — Security

If credentials are leaked:

  1. Rotate ALL affected keys immediately (Brevo, Cloudflare, Turnstile)
  2. Update Cloudflare Pages secrets: npx wrangler pages secret put <NAME>
  3. Update GitHub secrets: gh secret set <NAME>
  4. Check Brevo logs for unauthorized email sends
  5. Check Cloudflare audit logs for unauthorized deploys
  6. See full procedure in solanasis-credentials-security-playbook.md

If the site is defaced or compromised:

  1. Check GitHub commit history for unauthorized commits
  2. Roll back via Cloudflare Pages deployment history (Dashboard → Pages → Deployments)
  3. Rotate all secrets
  4. Review GitHub Actions audit log

Monitoring

WhatWhere
AnalyticsUmami Dashboard (cookie-free)
UptimeCloudflare → Analytics → Web Analytics
DeploysGitHub Actions → Workflows
Email deliveryBrevo → Transactional → Logs
Security eventsCloudflare → Security → Events
SSL certificateAuto-renewed by Cloudflare (90-day cycle)