---
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=<new-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="<new-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 "<new-token>"
```

Also update the `.env` file:
```
TUNNEL_TOKEN=<new-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
