Overview
PensionPortal.ai uses Cloudflare as its CDN, WAF, and DNS layer in front of Vercel. All traffic to *.pensionportal.ai passes through Cloudflare before reaching Vercel’s edge network. This provides DDoS protection, web application firewall rules, rate limiting, and TLS termination.
DNS Records
Configure the following records in Cloudflare Dashboard → DNS → Records for pensionportal.ai.
| Type | Name | Value | Proxy Status | Purpose |
|---|
CNAME | www | cname.vercel-dns.com | Proxied | Marketing site |
CNAME | app | cname.vercel-dns.com | Proxied | Application portal |
CNAME | docs | cname.vercel-dns.com | Proxied | Mintlify documentation |
CNAME | * | cname.vercel-dns.com | Proxied | Wildcard coverage |
TXT | @ | v=spf1 include:_spf.resend.com ~all | DNS only | Email SPF (Resend) |
TXT | resend._domainkey | DKIM value from Resend Dashboard | DNS only | Email DKIM |
TXT | _dmarc | v=DMARC1; p=quarantine; rua=mailto:dmarc@pensionportal.ai | DNS only | DMARC policy |
MX | @ | mx1.improvmx.com (priority 10), mx2.improvmx.com (priority 20) | DNS only | Email routing |
Email records (SPF, DKIM, DMARC, MX) must be set to DNS only (grey cloud). Proxying these record types through Cloudflare would break email delivery and DKIM signature validation.
After adding or modifying DKIM and DMARC records, allow up to 48 hours for DNS propagation before running DMARC verification. Validate with dig TXT _dmarc.pensionportal.ai and a DMARC checker tool.
TLS Configuration
Configure in Cloudflare Dashboard → SSL/TLS.
| Setting | Value | Notes |
|---|
| SSL Mode | Full (Strict) | Vercel certificates are verified end-to-end; rejects self-signed certs |
| Minimum TLS Version | TLS 1.2 (minimum) | Recommend TLS 1.3 only for DORA compliance |
| HSTS | Enabled | max-age=31536000; includeSubDomains; preload |
| Certificate | Cloudflare Universal SSL + Vercel Edge Certificate | Both auto-renewed |
SSL Mode must be Full (Strict), never Flexible. Flexible mode encrypts only the Cloudflare-to-browser connection, leaving the Cloudflare-to-Vercel leg unencrypted. This would violate GDPR Article 32 and DORA technical security requirements for a regulated financial application.
HSTS Configuration
Enable HSTS in Cloudflare Dashboard → SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS):
Max Age: 12 months (31536000 seconds)
Include Subdomains: ON
Preload: ON
No-Sniff Header: ON
Once HSTS with preload is submitted to the HSTS preload list, it is very difficult to reverse. Ensure all subdomains serve valid HTTPS before enabling preload. Confirm with the team before enabling.
WAF Configuration
Configure in Cloudflare Dashboard → Security → WAF.
Managed Rulesets
Enable the following managed rulesets under WAF → Managed Rules:
| Ruleset | Status | Notes |
|---|
| Cloudflare Managed Ruleset | ON | Covers common web exploits |
| Cloudflare OWASP Core Ruleset | ON | OWASP Top 10 protections |
| Cloudflare Exposed Credentials Check | ON | Detects use of known breached credentials |
Custom WAF Rules
Create the following rules in Security → WAF → Custom Rules:
Rule 1 — Geo-restrict regulated data endpoints
This rule challenges non-EU/non-Irish traffic attempting to access regulated pension data APIs. PensionPortal.ai operates under IORP II, which is an Irish/EU regulatory framework.
(ip.geoip.continent ne "EU" and ip.geoip.country ne "IE"
and http.request.uri.path matches "^/api/(schemes|members|employers|compliance).*")
Action: Challenge (Managed CAPTCHA)
Disable or modify this rule if expanding to non-EU markets (Wave 2). Ensure legal review of data residency obligations before disabling geo-restriction for any regulated endpoint.
Rule 2 — Protect RAG ingest endpoint
The RAG ingest endpoint triggers AI document processing and is not intended for public access. Block all traffic except from known infrastructure IPs.
(http.request.uri.path matches "^/api/rag/ingest.*")
Action: Block (allowlist known IPs via IP Lists)
Create an IP List in Cloudflare → Manage Account → Configurations → Lists, and reference it in the rule to permit your CI/admin IPs.
Rule 3 — Block common exploit patterns
(http.request.uri.query contains "../"
or http.request.uri.query contains "UNION SELECT")
Action: Block
These custom WAF rules supplement, but do not replace, secure server-side input validation. The application must validate and sanitize all user input regardless of WAF rules — WAF rules can be bypassed by sophisticated attackers and should be treated as defence-in-depth.
Rate Limiting Rules
Configure in Cloudflare Dashboard → Security → WAF → Rate Limiting Rules.
| Rule Name | Path Pattern | Threshold | Period | Action |
|---|
| Login Rate Limit | /api/auth/callback/credentials | 10 requests | 1 minute | Block for 10 minutes |
| AI Chat Rate Limit | /api/ai/chat | 30 requests | 1 minute | Throttle |
| API General Rate Limit | /api/* | 500 requests | 1 minute | Throttle |
| RAG Ingest Protection | /api/rag/ingest | 5 requests | 1 hour | Block |
The login rate limit (10 req/min) is intentionally strict to prevent credential stuffing attacks against trustee accounts. Legitimate users should not approach this threshold during normal use. If users report false positives, investigate before raising the limit.
Bot Protection
Configure in Cloudflare Dashboard → Security → Bots.
| Setting | Value | Notes |
|---|
| Bot Fight Mode | ON | Blocks known bad bots |
| Super Bot Fight Mode (Pro+) | Block definitely automated traffic | Requires Pro plan or higher |
| Browser Integrity Check | ON for /dashboard/* paths | Validates browser fingerprint |
| Challenge Passage | 30 minutes | Time before re-challenging solved CAPTCHA |
Super Bot Fight Mode may block legitimate API clients such as monitoring services (e.g., BetterUptime). Allowlist known monitoring IPs or user-agent strings in the Cloudflare firewall before enabling.
Zero Trust Configuration Plan
Cloudflare Zero Trust (formerly Cloudflare Access) provides identity-aware access control for sensitive admin paths, replacing IP-allowlisting with company identity authentication.
Target state (future hardening):
- Create a Cloudflare Access application scoped to
app.pensionportal.ai/admin/*
- Require authentication via Cloudflare Access + company email (Google Workspace or Azure AD IdP)
- Enable device posture checks — allow access only from managed, MDM-enrolled devices
- Log all access events to Cloudflare SIEM / Logpush (forward to your SIEM or audit log store)
- Conduct weekly access reviews — security lead reviews active users and revokes stale access
Zero Trust configuration requires a Cloudflare Zero Trust plan (free tier available for small teams). Review Cloudflare Access documentation before implementing.
Admin Route Protection — Current State
Until Zero Trust is fully configured, the following interim controls apply:
- No dedicated
/admin/* URL path exists. SuperAdmin capabilities are not exposed via a separate URL prefix.
- Admin users are identified by role in the JWT, not by URL path. The
SuperAdmin role is stored in the database and reflected in the session token.
- All
/api/* routes perform server-side role checks using the session JWT before executing privileged operations.
Role-based access in the JWT is only as secure as the AUTH_SECRET used to sign tokens. Ensure AUTH_SECRET is unique per environment, never committed to source control, and rotated if ever exposed. See the Vercel Deployment Guide for secret generation instructions.
Recommended next step: Implement a dedicated /admin route tree protected by both JWT role check and Cloudflare Access, providing defence-in-depth for the highest-privilege operations.
Configure in Cloudflare Dashboard → Speed → Optimization and Caching.
| Setting | Value | Notes |
|---|
| Auto Minify — HTML | ON | Reduces HTML payload size |
| Auto Minify — CSS | ON | Reduces CSS payload size |
| Auto Minify — JavaScript | ON | Reduces JS payload size |
| Brotli Compression | ON | More efficient than gzip for modern browsers |
| HTTP/3 with QUIC | ON | Reduces latency on unreliable connections |
| Rocket Loader | OFF | Incompatible with Next.js hydration — do not enable |
Cache Rules
Create a Cache Rule in Cloudflare Dashboard → Caching → Cache Rules:
Rule: Cache Next.js static assets at edge
If URI path matches: ^/_next/static/.*
Cache TTL: 1 year (31536000 seconds)
Browser TTL: 1 year
Edge TTL: 1 year
Cache Status: Cache Everything
Next.js content-hashes all files in /_next/static/, making long-lived caching safe — stale files are never served because filenames change with each build.
Do not cache /api/* paths or /dashboard/* paths at the Cloudflare edge. These routes return user-specific or real-time data. Vercel handles caching for these routes via the Next.js cache headers.
Analytics and Monitoring
| Feature | Target | Notes |
|---|
| Cloudflare Analytics | All zones | Traffic, bandwidth, threat insights |
| Web Analytics (privacy-preserving) | www.pensionportal.ai | No cookies, GDPR-compliant, no consent banner required |
| Security Events | Review weekly | Check for blocked requests, unusual geo patterns |
| Under Attack Mode | Activate during active DDoS only | JS challenge for all visitors; disables normal access |
Web Analytics setup:
Add the Cloudflare Web Analytics script to the marketing site (www.pensionportal.ai). This script does not use cookies and does not require a GDPR consent banner, making it suitable for a regulated financial platform.
Under Attack Mode should only be activated during an active, confirmed DDoS event. Enabling it outside of an incident will challenge all legitimate users — including pension trustees — with a browser integrity check, causing significant friction. Coordinate activation with the security lead and document the decision in the incident log.
Security Events review checklist (weekly):
- Check for any WAF rule triggers on
/api/auth/* — spikes may indicate credential stuffing
- Review blocked geo-restriction hits on regulated endpoints
- Check rate limit trigger counts — a significant increase may indicate automated probing
- Review any manual
Block actions and confirm they remain appropriate