--- type: methodology title: Cloudflare Infrastructure Setup created: 2026-05-11 tags: - methodology - cloudflare - infrastructure - networking --- # Cloudflare Infrastructure Setup ## Architecture Overview ``` Browser ──► Cloudflare (orange cloud) ──► Tunnel ──► cloudflared ──► Traefik:8081 │ (tunnel entrypoint) │ ┌──────────┴──────────┐ secureweb tunnel (direct internet) (via tunnel) :443 :8081 LetsEncrypt plain HTTP ``` All external traffic goes through Cloudflare Tunnel. Traefik serves as the internal reverse proxy, routing by Host header. Traefik's LetsEncrypt certs exist for LAN/direct access but aren't used externally (Cloudflare edge terminates TLS for visitors). ## Active Stack | Component | Location | Notes | |---|---|---| | Tunnel name | `home` | Created at Cloudflare Zero Trust → Networks → Tunnels | | Tunnel ID | `c29295c5-946a-4ddf-bdfe-7eafcd74faa3` | Visible in dashboard | | Token | `.env` `TUNNEL_TOKEN=...` | Used by cloudflared to authenticate | | cloudflared | production-1 Docker container | Runs with `--restart unless-stopped` | | Traefik | production-1 Docker container | Ports 80, 443, 8080, 8082 | | Traefik entrypoints | `web` (:80→redirect), `secureweb` (:443, TLS), `tunnel` (:8081), `metrics` (:8082) | | | Traefik config | `/docker/appdata/traefik/traefik.yaml` | | | Compose file | `/docker/compose/docker-compose.yaml` | cloudflared service defined but runs standalone due to interpolation issues | | Traefik labels | Per-service in docker-compose.yaml | Pattern: `entrypoints=tunnel`, `Host(`subdomain.gharbeia.net`)` | ## 1. Adding a Second Domain Example: adding `example.com` alongside `gharbeia.net`. ### Step 1: Cloudflare DNS 1. Go to Cloudflare Dashboard → Add site → enter `example.com` 2. Cloudflare scans existing DNS records — verify and continue 3. Change nameservers at your registrar to Cloudflare's 4. Wait for DNS to propagate ### Step 2: Cloudflare Tunnel — add hostname 1. **Zero Trust** → **Networks** → **Connectors** → **Cloudflare Tunnels** 2. Select tunnel **home** → **Edit** 3. Click **Add a public hostname** 4. Set: - **Subdomain:** `*` - **Domain:** `example.com` - **Service Type:** HTTP - **URL:** `traefik:8081` 5. Save Cloudflare DNS will automatically create a wildcard CNAME for `*.example.com` pointing to `c29295c5-946a-4ddf-bdfe-7eafcd74faa3.cfargotunnel.com`. ### Step 3: Traefik — update cert resolver Cloudflare wildcard certs only cover `*.gharbeia.net`. For `*.example.com`, either: **Option A: Add to Traefik's ACME config** In `/docker/appdata/traefik/traefik.yaml`, in `certificatesResolvers.letsencrypt.acme`: ```yaml dnsChallenge: provider: cloudflare resolvers: - 1.1.1.1:53 - 1.0.0.1:53 propagation: delayBeforeChecks: 120s # increase for wildcards ``` Then in `entryPoints.secureweb.http.tls.domains`: ```yaml domains: - main: gharbeia.net sans: - "*.gharbeia.net" - main: example.com sans: - "*.example.com" ``` **Important:** Before ACME DNS challenge can succeed for wildcard domains proxied via Cloudflare, you MUST create a DNS-only placeholder record for `_acme-challenge` to break the wildcard: Go to Cloudflare DNS → Add record: - **Type:** A - **Name:** `_acme-challenge` - **Content:** `127.0.0.1` - **Proxy:** DNS-only (gray cloud) Without this, the proxied wildcard CNAME intercepts `_acme-challenge.*` queries and LetsEncrypt can't verify the TXT challenge records. **Option B: Skip HTTPS cert for now** If the second domain doesn't need Traefik's own cert (i.e., Cloudflare edge certs are sufficient), no Traefik changes needed. The tunnel handles everything. ### Step 4: Add Traefik labels For each service on the new domain, add Traefik labels in docker-compose.yaml: ```yaml labels: - traefik.enable=true - traefik.http.routers.servicename.rule=Host(`subdomain.example.com`) - traefik.http.routers.servicename.entrypoints=tunnel - traefik.http.services.servicename.loadbalancer.server.port=3000 ``` Then restart the container to pick up labels (compose or docker run depending on env state). --- ## 2. Security Panic — Changing All Tokens and Settings ### Step 1: Cloudflare API Token 1. Go to Cloudflare Dashboard → **My Profile** → **API Tokens** 2. **Delete** the old token 3. **Create Token** → **Edit zone DNS** template: - Permissions: Zone → DNS → Edit - Zone Resources: Include → Specific zone → `gharbeia.net` - TTL: No expiration (or set a reasonable one) 4. Copy the new token ### Step 2: Update .env on production-1 ```bash ssh production-1 # Edit the token sed -i "s|^CLOUDFLARE_DNS_API_TOKEN=.*|CLOUDFLARE_DNS_API_TOKEN=|" /docker/compose/.env ``` ### Step 3: Restart Traefik Traefik runs standalone (not under compose). Restart it with the new token: ```bash docker rm -f traefik docker run -d --name traefik --restart unless-stopped --network networking \ -p 80:80 -p 443:443 -p 8080:8080 -p 8082:8082 \ -e CF_DNS_API_TOKEN="" \ -e TZ="America/New_York" \ -v /var/run/docker.sock:/var/run/docker.sock:ro \ -v /docker/appdata/logs/traefik:/var/log \ -v /docker/appdata/traefik:/etc/traefik \ -v /docker/appdata/traefik/letsencrypt:/letsencrypt \ traefik:latest ``` Traefik will automatically renew its LetsEncrypt cert with the new token. ### Step 4: Cloudflare Tunnel Token If you also rotate the tunnel token: 1. Go to **Zero Trust** → **Networks** → **Tunnels** → **home** 2. Click the three dots → **Recreate token** 3. Copy the new token On production-1: ```bash docker rm -f cloudflared docker run -d --name cloudflared --restart unless-stopped --network networking \ -v /docker/appdata/cloudflared:/home/nonroot/.cloudflared \ cloudflare/cloudflared:latest tunnel --no-autoupdate run --token "" ``` Also update the `.env` file: ``` TUNNEL_TOKEN= ``` ### Step 5: Verify everything works ```bash # Check cloudflared is connected docker logs cloudflared --tail 5 | grep "Registered tunnel connection" # Check Traefik is running and has routes docker logs traefik --tail 10 | grep "Register" # Test end-to-end curl -sI https://git.gharbeia.net/ | head -5 # Should return HTTP/2 200 ``` ### Step 6: Rotate other secrets (if needed) Other credentials in `.env` that may need rotation: - `AUTHENTIK_SECRET_KEY` — generate new: `openssl rand -base64 60 | tr -d '\n'` - `POSTGRESQL_PASSWORD` — generate new - `EMAIL_PASSWORD` — Fastmail app password - `GITEA_TOKEN` — Gitea settings → Applications - `OPENROUTER_API_KEY` — OpenRouter dashboard ## Troubleshooting ### Cert renewal fails with "Invalid format for Authorization header" The Cloudflare API token in `CF_DNS_API_TOKEN` is wrong or expired. Generate a new one and restart Traefik. ### Cert renewal stuck on "Waiting for DNS record propagation" Likely the proxied wildcard CNAME is intercepting `_acme-challenge` subdomain. Add a DNS-only A record for `_acme-challenge` → `127.0.0.1` in Cloudflare DNS to break the wildcard. ### Tunnel shows 502 Bad Gateway - Check cloudflared logs: `docker logs cloudflared --tail 10` - If `connection refused` → Traefik isn't listening on the expected port - If `tls: unrecognized name` → SNI mismatch (revert to HTTP mode in tunnel config) ### Adding a new service 1. Add Traefik labels to the service in docker-compose.yaml 2. If the container was started without labels, recreate it with `docker run` including `-l` flags (compose may fail due to .env interpolation issues) 3. The tunnel wildcard already catches `*.gharbeia.net` — no tunnel changes needed