|
| 1 | +# Deployment & Firewall Notes: Cloudflare Wildcards & Security |
| 2 | + |
| 3 | +This document captures the findings and architectural decisions regarding DNS, SSL (Let's Encrypt), Firewalls, and Cloudflare integration when hosting the LMS on Dokploy/Netcup. |
| 4 | + |
| 5 | +## The Problem: Traefik Timeout Errors |
| 6 | +When attempting to setup wildcard subdomains (`*.preciopana.com`) with Let's Encrypt on Traefik, we encountered two timeout errors: |
| 7 | +1. **HTTP Challenge Timeout (Port 80):** Let's Encrypt could not reach the server because the Netcup hardware firewall restricted ports 80 and 443 to *only* Cloudflare IPs. Since the wildcard record in Cloudflare was set to DNS Only (Grey Cloud), Let's Encrypt tried to connect directly (not via Cloudflare) and was blocked by Netcup. |
| 8 | +2. **DNS Challenge Timeout (Port 53):** Traefik could not reach Cloudflare's DNS (1.1.1.1:53) because UFW (Ubuntu's internal firewall) blocks Docker's outbound requests by default. (Since Netcup provides a hardware firewall, UFW is redundant and should be disabled). |
| 9 | + |
| 10 | +## The Cloudflare Limitation |
| 11 | +On Cloudflare Free/Pro/Business tiers, **wildcard DNS records (`*`) cannot be proxied (Orange Cloud)**. They must be DNS Only (Grey Cloud). This means wildcard traffic hits the server directly. If the server firewall only allows Cloudflare IPs, the wildcard traffic is completely blocked. |
| 12 | + |
| 13 | +## The Solution: Option B (Strict Cloudflare Proxying) |
| 14 | +To maintain maximum security (keeping the Netcup firewall restricted *only* to Cloudflare IPs and keeping all traffic Proxied/Orange Cloud), we cannot use a blanket `*` wildcard record. |
| 15 | + |
| 16 | +Instead, we must dynamically create an explicit `A` record for every new school via the Cloudflare API when a user finishes the `/create-school` flow. |
| 17 | + |
| 18 | +### Implementation Plan for Option B (To be done in the future) |
| 19 | + |
| 20 | +#### 1. Environment Variables |
| 21 | +Add these to `.env.local` and Dokploy environment variables: |
| 22 | +```env |
| 23 | +CLOUDFLARE_ZONE_ID=your_zone_id_here |
| 24 | +SERVER_IP=152.53.160.37 |
| 25 | +# CF_DNS_API_TOKEN is already present |
| 26 | +``` |
| 27 | + |
| 28 | +#### 2. Server Action (e.g., `app/actions/cloudflare.ts`) |
| 29 | +```typescript |
| 30 | +'use server' |
| 31 | + |
| 32 | +export async function createCloudflareSubdomain(slug: string) { |
| 33 | + const zoneId = process.env.CLOUDFLARE_ZONE_ID; |
| 34 | + const apiToken = process.env.CF_DNS_API_TOKEN; |
| 35 | + const serverIp = process.env.SERVER_IP; |
| 36 | + |
| 37 | + const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, { |
| 38 | + method: 'POST', |
| 39 | + headers: { |
| 40 | + 'Content-Type': 'application/json', |
| 41 | + 'Authorization': `Bearer ${apiToken}` |
| 42 | + }, |
| 43 | + body: JSON.stringify({ |
| 44 | + type: 'A', |
| 45 | + name: slug, // e.g., "myschool" |
| 46 | + content: serverIp, // Netcup IP |
| 47 | + ttl: 1, // Auto TTL |
| 48 | + proxied: true // Enables Cloudflare Proxy (Orange Cloud) |
| 49 | + }) |
| 50 | + }); |
| 51 | + |
| 52 | + if (!response.ok) { |
| 53 | + const errorData = await response.json(); |
| 54 | + console.error("Cloudflare Error:", errorData); |
| 55 | + throw new Error('Failed to create secure subdomain'); |
| 56 | + } |
| 57 | + |
| 58 | + return await response.json(); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +#### 3. Integration in `/create-school` Flow |
| 63 | +In `components/tenant/create-school-form.tsx`, call the new action right after the tenant gets inserted into the database: |
| 64 | +```typescript |
| 65 | +import { createCloudflareSubdomain } from "@/app/actions/cloudflare"; |
| 66 | + |
| 67 | +// Inside form submit handler: |
| 68 | +try { |
| 69 | + // 1. Create school in Supabase... |
| 70 | + |
| 71 | + // 2. Provision Cloudflare subdomain dynamically |
| 72 | + await createCloudflareSubdomain(data.slug); |
| 73 | + |
| 74 | + // 3. Show a loading state (e.g., waiting 5-10s for DNS propagation) |
| 75 | + |
| 76 | + // 4. Redirect |
| 77 | + window.location.href = `https://${data.slug}.preciopana.com`; |
| 78 | +} catch (error) { |
| 79 | + toast.error("Error creating school."); |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +**Next Steps for Migration:** |
| 84 | +- Remove the `*` record from Cloudflare. |
| 85 | +- Keep Netcup firewall restricted to Cloudflare IPs. |
| 86 | +- Implement the Cloudflare API integration. |
| 87 | +- Ensure Dokploy/Traefik expects specific subdomains or gracefully routes them. Since Cloudflare manages the SSL Edge Certificates for proxied records, Traefik doesn't strictly need to do ACME TLS challenges for them as long as Cloudflare communicates with Traefik over HTTP or uses a Cloudflare Origin Cert. |
0 commit comments