07 — Security and Environments
Dev Guide — Savoy Signature Hotels
PRD refs:15_Security_and_Data_Protection.md,02_Infrastructure_and_Environments.md
1. Overview
Section titled “1. Overview”This guide covers Azure infrastructure provisioning, secrets management, security headers, RBAC, environment configuration, CI/CD pipeline, and GDPR compliance. All environments run on Azure with Cloudflare as the edge layer.
2. Azure Infrastructure
Section titled “2. Azure Infrastructure”2.1 Services per Environment
Section titled “2.1 Services per Environment”| Service | SKU | Purpose |
|---|---|---|
| App Service (Next.js) | P1v3 or P2v3 | SSR app, Node.js 20.9+ LTS, auto-scale |
| App Service (Umbraco) | P1v3 or P2v3 | .NET 10 LTS, always-on |
| Azure SQL | Standard S2 (50 DTU) or General Purpose | Content database |
| Blob Storage | Standard LRS, Hot tier | Media files |
| Application Insights | Pay-as-you-go | APM for both apps |
| Log Analytics | Pay-as-you-go | Centralized logging (KQL queries) |
| Key Vault | Standard | Secrets management |
| Virtual Network | — | Private endpoints for SQL and Blob |
2.2 Scaling
Section titled “2.2 Scaling”| Component | Strategy |
|---|---|
| Next.js | Auto-scale: min 2, max 6 instances (trigger: CPU > 70%) |
| Umbraco | Single instance (content editing is not high-traffic) |
3. Environments
Section titled “3. Environments”3.1 Four Environments
Section titled “3.1 Four Environments”| Env | Domain Pattern | Resource Group | Deploy Trigger |
|---|---|---|---|
| DEV | savoy-dev-*.wycreative.com | rg-savoy-dev | PR from develop → deploy/dev (CI + 1 approver) |
| STAGE | savoy.stage-*.wycreative.com | rg-savoy-stage | Auto on PR merge to staging |
| QA | qa-*.savoysignature.com | rg-savoy-qa | Manual promotion |
| PROD | *.savoysignature.com / hotelnext.pt | rg-savoy-prod | Manual promotion + approval |
3.2 Environment Variables
Section titled “3.2 Environment Variables”| Variable | DEV | STAGE | QA | PROD |
|---|---|---|---|---|
UMBRACO_API_URL | https://savoy-dev-cms.wycreative.com | https://savoy-stage-cms.wycreative.com | https://qa-cms.savoysignature.com | https://cms.savoysignature.com |
CLOUDFLARE_ZONE_ID | Zone ID (Key Vault) | Zone ID | Zone ID | Zone ID |
CLOUDFLARE_API_TOKEN | Token (Key Vault) | Token | Token | Token |
NODE_ENV | development | staging | production | production |
REVALIDATE_SECRET | dev-secret | stage-secret | qa-secret | prod-secret |
NEXT_PUBLIC_GA_ID | — | — | — | G-XXXXXXXXXX |
APPINSIGHTS_KEY | Key | Key | Key | Key |
GATE_ENABLED | true | true | true | false |
GATE_SECRET | Secret (Key Vault) | Secret | Secret | — |
GATE_COOKIE_DOMAIN | .wycreative.com | .wycreative.com | — | — |
3.3 Server-Only vs Client-Side Variables
Section titled “3.3 Server-Only vs Client-Side Variables”| Exposure | Prefix | Examples |
|---|---|---|
| Server-only (never in browser) | No prefix | UMBRACO_API_KEY, CLOUDFLARE_API_TOKEN, REVALIDATE_SECRET, Mailjet keys |
| Client-side (exposed to browser) | NEXT_PUBLIC_ | NEXT_PUBLIC_RECAPTCHA_SITE_KEY, NEXT_PUBLIC_GA_ID |
Rule: NEVER expose API keys, tokens, or secrets with the NEXT_PUBLIC_ prefix.
4. Secrets Management
Section titled “4. Secrets Management”| Environment | Engine | Injection Method |
|---|---|---|
| Local | .env.local (git-ignored) | Next.js CLI / dotnet run |
| CI/CD | GitHub Actions Secrets | Build context variables |
| Azure | Azure Key Vault | Managed Identity at runtime |
Key Vault secrets include: UMBRACO_API_KEY, CLOUDFLARE_API_TOKEN, REVALIDATE_SECRET, AZURE_BLOB_CONNECTION_STRING, mailjet-api-key, mailjet-secret-key, Azure SQL connection string.
4.1 Key Vault Secret Names
Section titled “4.1 Key Vault Secret Names”| Secret Name | Purpose | Used by |
|---|---|---|
umbraco-api-key | Delivery API authentication | Next.js |
revalidate-secret | Webhook HMAC verification | Next.js |
sql-connection-string | Azure SQL connection | Umbraco |
blob-connection-string | Azure Blob Storage connection | Umbraco |
mailjet-api-key | Mailjet SMTP username | Umbraco |
mailjet-secret-key | Mailjet SMTP password | Umbraco |
cloudflare-zone-id | Cloudflare zone ID for cache purge | Next.js |
cloudflare-api-token | Cloudflare API token (Cache Purge permission) | Next.js |
Cache purge is disabled by default. The webhook handler checks
CLOUDFLARE_PURGE_ENABLED=truebefore purging. Set this App Setting totruewhen Cloudflare proxy is active and cache purge is ready to test.
All secret names use kebab-case. Each environment (kv-savoy-dev, kv-savoy-stage, kv-savoy-prod) has its own Key Vault with its own values.
5. Email / SMTP (Mailjet)
Section titled “5. Email / SMTP (Mailjet)”Mailjet is the SMTP relay for all email sent by the Umbraco CMS. Next.js does not send emails.
5.1 Architecture
Section titled “5.1 Architecture”| Item | Value |
|---|---|
| Provider | Mailjet (SMTP relay) |
| SMTP Host | in-v3.mailjet.com |
| Port | 587 (STARTTLS) |
| Sender | Only Umbraco CMS |
| From (dev/staging) | noreply@wycreative.com |
| From (prod) | noreply@savoysignature.com |
| Configuration | appsettings.json → Umbraco.CMS.Global.Smtp |
| Credentials | Azure Key Vault (mailjet-api-key + mailjet-secret-key) |
5.2 Mailjet Account Setup
Section titled “5.2 Mailjet Account Setup”- Create a Mailjet account at app.mailjet.com
- Two accounts (or subaccounts) are required:
- Non-production — for dev and staging environments
- Production — for the live sites
- In each account, go to Account Settings > API Keys and note the API Key and Secret Key
5.3 Domain Verification (DNS)
Section titled “5.3 Domain Verification (DNS)”Mailjet requires sender domain verification. Configure these DNS records in Cloudflare:
| Record | Type | Name | Value | Environment |
|---|---|---|---|---|
| SPF | TXT | @ | v=spf1 include:spf.mailjet.com ~all | Both |
| DKIM | TXT | mailjet._domainkey | (provided by Mailjet dashboard) | Both |
| DMARC | TXT | _dmarc | v=DMARC1; p=none; rua=mailto:dmarc@... | Both |
Domains to verify:
wycreative.com(non-prod sender)savoysignature.com(prod sender)
Mailjet dashboard guides through this process: Account Settings > Sender Domains & Addresses > Manage > DNS Setup.
5.4 Azure Key Vault Secrets
Section titled “5.4 Azure Key Vault Secrets”Add the Mailjet credentials to the Key Vault for each environment:
# DEV environmentaz keyvault secret set --vault-name kv-savoy-dev --name mailjet-api-key --value "YOUR_DEV_API_KEY"az keyvault secret set --vault-name kv-savoy-dev --name mailjet-secret-key --value "YOUR_DEV_SECRET_KEY"
# STAGING environmentaz keyvault secret set --vault-name kv-savoy-stage --name mailjet-api-key --value "YOUR_DEV_API_KEY"az keyvault secret set --vault-name kv-savoy-stage --name mailjet-secret-key --value "YOUR_DEV_SECRET_KEY"
# PRODUCTION environmentaz keyvault secret set --vault-name kv-savoy-prod --name mailjet-api-key --value "YOUR_PROD_API_KEY"az keyvault secret set --vault-name kv-savoy-prod --name mailjet-secret-key --value "YOUR_PROD_SECRET_KEY"Dev and staging share the same non-prod Mailjet account. Production uses the separate prod account.
5.5 App Settings (Bicep)
Section titled “5.5 App Settings (Bicep)”The Umbraco App Service already has Bicep entries that reference Key Vault (added in infra/modules/app-service-umbraco.bicep):
Umbraco__CMS__Global__Smtp__Username → @Microsoft.KeyVault(VaultName=kv-savoy-{env};SecretName=mailjet-api-key)Umbraco__CMS__Global__Smtp__Password → @Microsoft.KeyVault(VaultName=kv-savoy-{env};SecretName=mailjet-secret-key)The From address is set in appsettings.json (dev) and appsettings.Production.json (all Azure envs). To override per environment, add an App Setting in Azure Portal:
Umbraco__CMS__Global__Smtp__From = noreply@wycreative.com5.6 Local Development
Section titled “5.6 Local Development”For local development (dotnet run), the SMTP credentials in appsettings.json are placeholders. To send real emails locally:
- Replace
MAILJET_API_KEY_PLACEHOLDERandMAILJET_SECRET_KEY_PLACEHOLDERinappsettings.jsonwith real non-prod Mailjet keys - Never commit real credentials — the placeholders are safe to commit
Alternatively, create appsettings.Development.json (git-ignored) with the real credentials.
5.7 Email Service Code
Section titled “5.7 Email Service Code”| File | Purpose |
|---|---|
Email/EmailComposer.cs | DI registration (auto-discovered by Umbraco) |
Email/Services/IEmailService.cs | Send interface (SendAsync, SendFormSubmission, etc.) |
Email/Services/SmtpEmailService.cs | Implementation using Umbraco’s IEmailSender |
Email/Services/IEmailTemplateRenderer.cs | Razor template renderer interface |
Email/Services/RazorEmailTemplateRenderer.cs | Razor.Templating.Core implementation |
Email/Models/ | EmailBaseModel, FormSubmission, FormConfirmation, NewsletterWelcome |
Email/Templates/ | Razor views: _EmailLayout, FormSubmission, FormConfirmation, NewsletterWelcome |
5.8 Storybook Preview
Section titled “5.8 Storybook Preview”Email templates have visual preview stories in Storybook under Email/:
Email/Form Submission— Default, LongContent, MinimalContentEmail/Form Confirmation— Default, PerHotelEmail/Newsletter Welcome— Default, PerHotel
Run pnpm --filter storybook dev and navigate to the Email section.
5.9 Spec & Plan References
Section titled “5.9 Spec & Plan References”- Spec:
docs/superpowers/specs/2026-03-17-smtp-mailjet-configuration-design.md - Plan:
docs/superpowers/plans/2026-03-17-smtp-mailjet-configuration.md
6. Cloudflare Security
Section titled “6. Cloudflare Security”6.1 Perimeter Defense
Section titled “6.1 Perimeter Defense”| Layer | Configuration |
|---|---|
| WAF | Managed Ruleset (Strict) — blocks SQLi, XSS, CMS exploits |
| Bot Management | Super Bot Fight Mode enabled |
| Rate Limiting | 20 req/min per IP on /api/forms/* and /api/search |
| DDoS | Unmetered L3/L4/L7 mitigation |
| SSL/TLS | Full (Strict), edge + origin certificates |
| Min TLS Version | 1.2 |
6.2 Cloudflare Zone Settings
Section titled “6.2 Cloudflare Zone Settings”| Setting | Value |
|---|---|
| Always HTTPS | On |
| HTTP/2 | On |
| HTTP/3 (QUIC) | On |
| Brotli | On |
| Auto Minify | HTML, CSS, JS |
6.3 DEV Subdomain Configuration (wycreative.com)
Section titled “6.3 DEV Subdomain Configuration (wycreative.com)”All internal development and staging environments use subdomains under wycreative.com (Cloudflare zone already active).
Domain Naming Convention
Section titled “Domain Naming Convention”| Service | Pattern | Example |
|---|---|---|
| Next.js (per hotel) | savoy.dev-{hotel-short}.wycreative.com | savoy.dev-signature.wycreative.com |
| Umbraco CMS | savoy.dev-cms.wycreative.com | — |
Complete DEV subdomain list (8 hotel sites + CMS):
| Subdomain | Site Key | Azure Target |
|---|---|---|
savoy.dev-signature.wycreative.com | savoy-signature | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-palace.wycreative.com | savoy-palace | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-royal.wycreative.com | royal-savoy | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-saccharum.wycreative.com | saccharum | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-reserve.wycreative.com | the-reserve | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-calheta.wycreative.com | calheta-beach | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-gardens.wycreative.com | gardens | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-next.wycreative.com | hotel-next | app-nextjs-savoy-dev.azurewebsites.net |
savoy.dev-cms.wycreative.com | — | app-umbraco-savoy-dev.azurewebsites.net |
All 8 hotel sites point to the same Next.js App Service — the site-resolver middleware uses the hostname to determine which hotel to serve.
Step-by-Step: Adding DNS Records in Cloudflare
Section titled “Step-by-Step: Adding DNS Records in Cloudflare”Prerequisites:
- Access to Cloudflare dashboard for
wycreative.comzone - Azure App Service already deployed (
app-nextjs-savoy-dev.azurewebsites.netandapp-umbraco-savoy-dev.azurewebsites.net)
1. Add CNAME records
In Cloudflare dashboard: DNS > Records > Add record
For each hotel site (repeat 8 times):
| Type | Name | Target | Proxy status | TTL |
|---|---|---|---|---|
| CNAME | savoy-dev-signature | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
| CNAME | savoy-dev-palace | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
| CNAME | savoy-dev-royal | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
| CNAME | savoy-dev-saccharum | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
| CNAME | savoy-dev-reserve | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
| CNAME | savoy-dev-calheta | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
| CNAME | savoy-dev-gardens | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
| CNAME | savoy-dev-next | app-nextjs-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
For the CMS:
| Type | Name | Target | Proxy status | TTL |
|---|---|---|---|---|
| CNAME | savoy-dev-cms | app-umbraco-savoy-dev.azurewebsites.net | Proxied (orange cloud) | Auto |
For Storybook (Cloudflare Pages):
| Type | Name | Target | Proxy status | TTL |
|---|---|---|---|---|
| CNAME | savoy-dev-storybook | savoy-storybook-dev.pages.dev | Proxied (orange cloud) | Auto |
DEV uses Cloudflare proxy (orange cloud) — all subdomains are proxied for consistent security (auth gate), performance monitoring, and SSL. The Dev Auth Gate requires Umbraco backoffice login before accessing any DEV site. Direct Azure URLs (
*.azurewebsites.net) bypass the auth gate and should only be used for emergency debugging.
2. Configure Azure Custom Domains
After adding DNS records, bind the custom domains in Azure Portal:
For the Next.js App Service (app-nextjs-savoy-dev):
- Go to App Service > Custom domains > Add custom domain
- Add each hostname:
savoy.dev-signature.wycreative.com,savoy.dev-palace.wycreative.com, etc. - Azure validates the CNAME record automatically
- Enable App Service Managed Certificate (free) for each domain — provides SSL automatically
- Repeat for all 8 hotel subdomains
For the Umbraco App Service (app-umbraco-savoy-dev):
- Same process: Custom domains > Add custom domain
- Add
savoy.dev-cms.wycreative.com - Enable App Service Managed Certificate
Note: Azure App Service Managed Certificates work with DNS-only CNAME records. No Cloudflare origin certificate needed for DEV.
3. Verify
# Test DNS resolutionnslookup savoy.dev-signature.wycreative.com# Expected: CNAME → app-nextjs-savoy-dev.azurewebsites.net → Azure IP
# Test HTTPScurl -I https://savoy.dev-signature.wycreative.com# Expected: 200 OK with X-Powered-By or similar header
# Test CMScurl -I https://savoy.dev-cms.wycreative.com/umbraco# Expected: 302 redirect to loginStaging Subdomains (Future)
Section titled “Staging Subdomains (Future)”Staging follows the same pattern with stage instead of dev, but with Cloudflare proxy enabled (orange cloud) to match production behavior:
| Subdomain | Target | Proxy |
|---|---|---|
savoy.stage-signature.wycreative.com | app-nextjs-savoy-stage.azurewebsites.net | Proxied (orange cloud) |
savoy.stage-cms.wycreative.com | app-umbraco-savoy-stage.azurewebsites.net | Proxied (orange cloud) |
Staging App Services do not exist yet — create them when provisioning the staging environment (see Section 16).
7. Umbraco CMS Security
Section titled “7. Umbraco CMS Security”7.1 Backoffice Access
Section titled “7.1 Backoffice Access”/umbracopath is NOT publicly accessible- Locked via Cloudflare Zero Trust or Azure IP Restrictions
- Only internal IPs and VPN users can access the backoffice
7.2 Identity Provider
Section titled “7.2 Identity Provider”| Setting | Value |
|---|---|
| Provider | Azure AD (Microsoft Entra ID) via OpenID Connect |
| Local accounts | Disabled (except emergency admin) |
| MFA | Enforced at Azure AD level |
7.3 RBAC (Role-Based Access Control)
Section titled “7.3 RBAC (Role-Based Access Control)”Editors are scoped to specific hotel nodes:
| Role | Access |
|---|---|
| Savoy Palace Editor | Only Savoy Palace siteRoot and its children |
| Royal Savoy Editor | Only Royal Savoy siteRoot and its children |
| Group Admin | All 8 sites + Shared Content |
| Developer | Full backoffice access |
Configuration: Create User Groups in Umbraco with “Start Node” set to the corresponding siteRoot node.
7.4 Dependency Scanning
Section titled “7.4 Dependency Scanning”- Dependabot enabled for NuGet CVE scanning
- CI/CD pipeline fails on high-severity vulnerabilities during
dotnet restore
8. Next.js Security Headers
Section titled “8. Next.js Security Headers”Configure in next.config.ts:
const 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' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { 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://savoymedia.blob.core.windows.net https://res.cloudinary.com", "font-src 'self' data:", "connect-src 'self' https://api.navarino.co https://*.google-analytics.com", ].join('; '), },];9. Webhook Security
Section titled “9. Webhook Security”| Mechanism | Implementation |
|---|---|
| HMAC SHA-256 | Verify X-Webhook-Signature header using REVALIDATE_SECRET |
| IP Allowlist | Restrict to Umbraco Azure App Service outbound IP |
import { createHmac } from 'crypto';
function verifyWebhookSignature(body: string, signature: string): boolean { const expected = createHmac('sha256', process.env.REVALIDATE_SECRET!) .update(body) .digest('hex'); return signature === expected;}10. Data Protection / GDPR
Section titled “10. Data Protection / GDPR”9.1 Encryption
Section titled “9.1 Encryption”| Scope | Method |
|---|---|
| At rest (SQL) | Azure SQL TDE with Microsoft-managed keys |
| At rest (Blob) | Azure Storage TDE |
| In transit | TLS 1.3 (fallback 1.2) |
9.2 Data Retention
Section titled “9.2 Data Retention”| Data | Retention | Deletion |
|---|---|---|
| Form submissions | 90 days | Scheduled task auto-delete |
| App Insights logs | 30 days | Stripped of request bodies |
| Cloudflare logs | 30 days | Automatic |
| Azure SQL backups | 35 days | PITR (Point-in-Time Restore) |
| CI/CD logs | 90 days | Automatic |
9.3 Cookie Consent
Section titled “9.3 Cookie Consent”| Setting | Value |
|---|---|
| CMP Provider | Cookiebot.com |
| Script injection | layout.tsx <head> |
| Blocking behavior | Automatic cookie blocking (GTM, Navarino, etc. blocked before consent) |
| Essential cookies (exempt) | Locale routing, CSRF, Cloudflare tokens |
| Monitoring | DPO receives monthly scan reports; unclassified cookies trigger alert |
11. Branch Strategy
Section titled “11. Branch Strategy”Branch protection:
| Branch | Rules |
|---|---|
develop | 1 approval required |
staging | 2 approvals + CI pass |
main | 2 approvals + QA sign-off + CI pass |
Feature branch naming: feature/SAVOY-{taskId}-{short-description} or fix/SAVOY-{taskId}-{short-description}
12. CI/CD Pipeline
Section titled “12. CI/CD Pipeline”11.1 Pipeline Stages
Section titled “11.1 Pipeline Stages”11.2 CI Commands
Section titled “11.2 CI Commands”pnpm install --frozen-lockfilepnpm lintpnpm typecheckpnpm test --coveragepnpm test:e2epnpm buildpnpm build:storybook11.3 openClaw AI QA Agent
Section titled “11.3 openClaw AI QA Agent”Runs on a local Mac runner. Steps:
- PR Pickup
- Performance Tests (Lighthouse CI: LCP < 2.5s, FID < 100ms, CLS < 0.1)
- Accessibility Audit (axe-core + Playwright)
- Visual Regression (Chromatic/Percy)
- UI/UX Validation (Playwright E2E across viewports)
- Posts comments to PR + Zoho Project
13. Monitoring & Alerts
Section titled “13. Monitoring & Alerts”| Metric | Threshold | Alert Level |
|---|---|---|
| Response Time P95 | > 2s | Warning |
| Response Time P95 | > 5s | Critical |
| Error Rate 5xx | > 1% | Critical |
| Cache Hit Rate | < 80% | Warning |
| CPU Usage | > 85% for 5min | Warning |
| Memory Usage | > 90% | Critical |
| SQL DTU Usage | > 80% | Warning |
Logging retention: Next.js + Umbraco: 90 days (App Insights). Cloudflare: 30 days. Azure SQL: 30 days. CI/CD: 90 days.
14. Disaster Recovery
Section titled “14. Disaster Recovery”| Component | Strategy | RTO | RPO |
|---|---|---|---|
| Azure SQL | Geo-replication | 1h | 5min |
| Blob Storage | GRS (Geo-Redundant Storage) | 1h | 0 |
| App Services | Re-deploy from CI/CD to secondary region | 2h | — |
| SQL Backups | PITR, 35-day retention | — | — |
15. Estimated Monthly Cost
Section titled “15. Estimated Monthly Cost”| Tier | Range |
|---|---|
| Minimum (DEV-like) | ~EUR 625/mo |
| Production (full stack) | ~EUR 1,450/mo |
16. Step-by-Step: Provisioning a New Environment
Section titled “16. Step-by-Step: Provisioning a New Environment”- Create Resource Group —
rg-savoy-{env} - Create Azure SQL — Standard S2, enable TDE, configure geo-replication for PROD
- Create Storage Account — Standard LRS Hot tier, create
mediaandform-uploadscontainers - Create Key Vault — Add all secrets (API keys, connection strings, tokens, Mailjet keys — see Section 5.4)
- Create App Service (Umbraco) — .NET 10 LTS, always-on, configure Managed Identity for Key Vault
- Create App Service (Next.js) — Node.js 20.9+, configure auto-scale rules
- Configure VNet — Private endpoints for SQL and Blob
- Create Application Insights — Link to both App Services
- Configure Cloudflare — DNS records, SSL, WAF rules, page rules, Zero Trust for
/umbraco - Deploy — Run CI/CD pipeline targeting the new environment
- Verify — Smoke tests, check monitoring alerts, validate RBAC
17. Common Pitfalls
Section titled “17. Common Pitfalls”| Pitfall | Solution |
|---|---|
| Secrets committed to git | Use .env.local (git-ignored) locally, Key Vault in Azure |
NEXT_PUBLIC_ prefix on sensitive keys | Only analytics/reCAPTCHA site keys get NEXT_PUBLIC_ prefix |
| Backoffice publicly accessible | Lock /umbraco via Cloudflare Zero Trust or Azure IP restrictions |
| Missing MFA for CMS users | Enforce MFA at Azure AD level |
| Local CMS accounts active | Disable local accounts, use Azure AD SSO only |
| Form data retained indefinitely | Configure 90-day auto-delete scheduled task |
| Missing security headers | Configure all headers in next.config.ts (see Section 7) |
| Dependabot alerts ignored | CI/CD must fail on high-severity NuGet vulnerabilities |
| Mailjet credentials committed to git | Use MAILJET_API_KEY_PLACEHOLDER in appsettings.json, real keys only in Key Vault |
| Mailjet sender domain not verified | Emails will be rejected — verify SPF + DKIM in Cloudflare DNS (see Section 5.3) |