15 — Security and Data Protection
PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs:02_Infrastructure_and_Environments.md,08_API_Contracts.md,12_Forms_and_Data_Collection.md
1. Purpose
Section titled “1. Purpose”This document outlines the security architecture for the Savoy Signature headless platform. It defines boundary protections, environment variables management, API security, automated scanning, and GDPR compliance.
2. Threat Model and Defense Layers
Section titled “2. Threat Model and Defense Layers”The headless architecture naturally mitigates many traditional CMS vulnerabilities by removing direct public access to the CMS database and backend code. However, new attack vectors (API abuse, unauthorized data exfiltration) must be secured.
2.1 Perimeter Defense (Cloudflare Pro)
Section titled “2.1 Perimeter Defense (Cloudflare Pro)”Cloudflare is the first line of defense for both the Next.js frontend and the Umbraco backend.
| Protection Layer | Configuration | Purpose |
|---|---|---|
| WAF (Web Application Firewall) | Cloudflare Managed Ruleset (Strict) | Blocks SQLi, XSS, and known CMS exploits before they reach Azure. |
| Bot Management | Super Bot Fight Mode (Enabled) | Challenges or blocks automated scrapers, form-fill bots, and vulnerability scanners. |
| Rate Limiting | Cloudflare WAF Rate Limiting Rules (see section 2.2) | Prevents denial-of-service and brute-force attacks on custom endpoints. Enforced at the edge, NOT in application code. |
| DDoS Protection | Unmetered L3/L4/L7 mitigation | Keeps the site online during volumetric attacks. |
| SSL/TLS | Full (Strict) | Enforces end-to-end encryption. Cloudflare issues edge certificates; Azure holds origin certificates. |
2.2 Rate Limiting Rules (Cloudflare WAF)
Section titled “2.2 Rate Limiting Rules (Cloudflare WAF)”Updated: 2026-04-13 — The rate limiting rule set is now plan-dependent because DEV/STAGE run on Cloudflare Free (1 rule per zone, 10s period) and QA/PROD will run on Cloudflare Pro. See
02_Infrastructure_and_Environments.md§ 3.4 for the plan matrix.
See
docs/PRD/23_Cloudflare_Strategy.md§ 5 for the complete rate limiting matrix per plan.
Rate limiting is enforced entirely at the Cloudflare WAF layer via Rate Limiting Rules. The Next.js application does not run an in-memory rate limiter — the previous apps/web/src/lib/rate-limit.ts has been removed because:
- In-memory state is incompatible with horizontal scaling (multi-instance App Service).
- Cloudflare blocks abusive traffic at the edge, before it ever reaches the origin.
- The app is now fully stateless, enabling edge caching and horizontal autoscaling.
Rules are managed by scripts/cloudflare/setup-rate-limits.sh which switches between rule sets via the CF_PLAN=free|pro environment variable.
Current state — DEV/STAGE (Cloudflare Free, 1 combined rule)
Section titled “Current state — DEV/STAGE (Cloudflare Free, 1 combined rule)”| Rule | Path(s) | Method | Limit | Mitigation Timeout | Purpose |
|---|---|---|---|---|---|
| Forms + Gate login | /api/forms/* OR /api/gate/login | POST | 2 req / 10s per IP | 10s | Combined protection — Free plan allows only 1 rule per zone with 10s period |
Target state — QA/PROD (Cloudflare Pro, 3 granular rules)
Section titled “Target state — QA/PROD (Cloudflare Pro, 3 granular rules)”| Rule | Path | Method | Limit | Mitigation Timeout | Purpose |
|---|---|---|---|---|---|
| Forms | /api/forms/* | POST | 5 req / 60s per IP | 60s | Block form spam and brute-force submissions |
| Search | /api/search | any | 20 req / 60s per IP | 60s | Prevent search scraping / DoS |
| Gate login | /api/gate/login | POST | 10 req / 60s per IP | 300s (5 min) | Anti-brute-force on Dev Auth Gate login |
Pro plan provisioning is required before QA goes live. The granular 3-rule set, 60s+ periods, and longer mitigation timeouts are not available on Free.
Important:
- The app still extracts the client IP (from
CF-Connecting-IP/X-Forwarded-For) for audit logging only — never for rate limit enforcement. - Rule definitions live in the Cloudflare ruleset (phase:
http_ratelimit). Running the script PUTs the entire ruleset idempotently. - See
docs/dev-backend-guides/10_CLOUDFLARE_CACHE_RULES.md§ Rate Limiting for expressions, plan comparison and verification.
3. Umbraco CMS Security (Backend)
Section titled “3. Umbraco CMS Security (Backend)”The CMS is the ultimate source of truth and must be heavily insulated.
3.1 Network Isolation
Section titled “3.1 Network Isolation”- The Umbraco backoffice (
/umbraco) is not publicly accessible. - Cloudflare Zero Trust (Access) or Azure IP Restrictions lock down the backoffice URL to specific corporate IP addresses (e.g., WYcreative office, Savoy HR/Marketing VPNs).
- The Content Delivery API (
/umbraco/delivery/api/v2) is only accessible via the Next.js frontend origin.
3.2 Dev Auth Gate (Non-Production Environments)
Section titled “3.2 Dev Auth Gate (Non-Production Environments)”All non-production environments (DEV, Stage, QA) are protected by an authentication gate that blocks public access. Users must log in with Umbraco backoffice credentials before viewing any content.
| Feature | Implementation |
|---|---|
| Auth Method | Umbraco backoffice user credentials (email + password) |
| Token | JWT (__dev_gate cookie), signed with HMAC-SHA256 |
| TTL | 7 days |
| Cookie Scope | .wycreative.com (shared across all DEV subdomains) |
| Next.js | Middleware checks cookie; redirects to /gate/login if invalid |
| Storybook | Cloudflare Pages Function (_middleware.js) checks same cookie |
| Umbraco Endpoint | POST /api/auth/validate-backoffice (validates credentials, protected by X-Gate-Key header) |
| Production | Gate disabled (GATE_ENABLED=false) — sites are public |
| Exemptions | /api/health, /api/webhooks/*, /_next/static/* |
Spec: docs/superpowers/specs/2026-03-19-dev-auth-gate-design.md
3.3 Authentication and Authorization
Section titled “3.3 Authentication and Authorization”- Identity Provider: Umbraco uses Azure Active Directory (Microsoft Entra ID) via OpenID Connect. Local CMS accounts are disabled for all users except emergency admins.
- MFA: Multi-Factor Authentication is enforced at the Azure AD level for all editors.
- Role-Based Access Control (RBAC): Editors are restricted to specific hotel nodes. For example, a “Savoy Palace Editor” cannot edit “Royal Savoy” content.
3.4 Dependency Management
Section titled “3.4 Dependency Management”- Automated Scanning: GitHub Dependabot is enabled on the
apps/cmsrepository to scan NuGet packages for CVEs. - Patch Management: The CI/CD pipeline fails if high-severity vulnerabilities are detected during the
dotnet restorephase.
4. Next.js App Security (Frontend)
Section titled “4. Next.js App Security (Frontend)”The Node.js/React layer requires strict data handling and header configurations.
4.1 Security Headers
Section titled “4.1 Security Headers”The following headers are injected via next.config.ts:
// next.config.ts headers configurationconst securityHeaders = [ { key: 'X-DNS-Prefetch-Control', value: 'on' }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'X-XSS-Protection', value: '1; mode=block' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' // Prevents clickjacking }, { key: 'X-Content-Type-Options', value: 'nosniff' // Prevents MIME-type sniffing }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { // Content Security Policy (CSP) key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google-analytics.com https://www.googletagmanager.com https://*.recaptcha.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.blob.core.windows.net https://savoysignature.com/cdn-cgi/image/; font-src 'self' data:; connect-src 'self' https://api.navarino.co https://*.google-analytics.com https://connect.facebook.net https://consent.cookiebot.com;" }];4.2 Secrets Management
Section titled “4.2 Secrets Management”Environment variables and connection strings are never committed to version control.
| Environment | Secrets Engine | Injection Method |
|---|---|---|
| Local | .env.local (git-ignored) | Next.js CLI / dotnet run |
| CI/CD | GitHub Actions Secrets | Passed to build context |
| Azure (All) | Azure Key Vault | App Services fetch secrets via Managed Identity at runtime. |
4.3 Client-Side Token Exposure
Section titled “4.3 Client-Side Token Exposure”- Tokens required by the browser (e.g.,
NEXT_PUBLIC_RECAPTCHA_SITE_KEY) are explicitly prefixed. - API keys for Umbraco (
UMBRACO_API_KEY), Cloudflare (CLOUDFLARE_API_TOKEN), and Mailjet are restricted to the Node.js context and never exposed to the client bundle.
5. Cache Purge Security
Section titled “5. Cache Purge Security”Cache purge is handled entirely within the CMS via Umbraco notification handlers (CachePurgeService). There is no webhook endpoint between CMS and Next.js for cache purge — all Cloudflare API calls originate from the CMS server.
Cloudflare credentials (ZoneId, ApiToken) are stored CMS-side only:
- Local development:
appsettings.Development.json(ordotnet user-secrets) - Production: Azure Key Vault, accessed via Managed Identity
REVALIDATE_SECRET is used exclusively for draft/preview mode (/api/draft endpoint in Next.js) — it is NOT used for cache purge.
Purge Log: All purge operations are logged to the SavoyPurgeLog SQL table, providing an auditable record of every cache purge event, URLs purged, and any errors.
6. Data Protection & GDPR
Section titled “6. Data Protection & GDPR”Savoy Signature processes PII (Personally Identifiable Information) via forms and analytics.
6.1 Data at Rest & in Transit
Section titled “6.1 Data at Rest & in Transit”- At Rest: The Azure SQL database (Umbraco content/forms) and Azure Blob Storage (media/uploads) utilize TDE (Transparent Data Encryption) with Microsoft-managed keys.
- In Transit: All traffic is mandated over TLS 1.3 (fallback 1.2).
6.2 Data Minimization & Retention
Section titled “6.2 Data Minimization & Retention”- Form Submissions: Stored in Umbraco Forms to handle email failure retries. Auto-deleted via a scheduled task after 90 days (configurable per form).
- Log Data: Azure Application Insights retains application logs for 90 days. Log payloads are stripped of request bodies (e.g., passwords, credit card info).
6.3 Cookie Consent (Cookiebot)
Section titled “6.3 Cookie Consent (Cookiebot)”The platform utilizes Cookiebot.com as the official Consent Management Platform (CMP).
- Implementation: The Cookiebot script (
<script id="Cookiebot" ...>) is injected into the<head>of thelayout.tsxfile for all 8 sites. - Auto-blocking: Cookiebot’s automatic cookie blocking prevents GTM, Navarino tracking, and other third-party scripts from executing before explicit user consent is granted.
- Exemptions: Essential cookies (e.g., Next.js locale routing, CSRF tokens, Cloudflare protection tokens) are classified strictly non-tracking and exempt from blocking.
- Alerts/Audits: The DPO receives monthly automated scan reports from Cookiebot. Unclassified cookies trigger an alert requiring manual categorization by the dev team.
7. Acceptance Criteria
Section titled “7. Acceptance Criteria”- Cloudflare WAF is blocking known exploits across all domains.
- Umbraco backoffice is inaccessible from unauthorized IP addresses.
- Next.js HTTP response headers achieve an “A” grade on SecurityHeaders.com.
- No secrets or API keys are present in the frontend bundle (verified via bundle analysis).
- CMS-native cache purge credentials (Cloudflare ZoneId, ApiToken) are stored in Key Vault and accessible only from the CMS App Service.
- Form payloads containing PII are encrypted at rest and auto-erased after 90 days.
- Azure Key Vault is successfully utilized for all production secrets via Managed Identity.
Outstanding items
Section titled “Outstanding items”- Provision Cloudflare Pro plan before QA go-live (~$20/mo per zone). Required to deploy the 3 granular rate limit rules (forms / search / gate login) with 60s+ periods. DEV/STAGE remain on Free with a single combined rule (2 req / 10s) as a stop-gap.
Next document: 16_Analytics_and_Tracking.md