> For the complete documentation index, see [llms.txt](https://docs.layeronecloud.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.layeronecloud.com/paas/cloudflare-setup.md).

# Cloudflare setup for the application platform

What to create in Cloudflare, in order, and what to put in the environment.

What to create in Cloudflare, in order, and what to put in the environment. `apps/paas/edge.py` does the rest.

Nothing here opens an inbound port. The connector dials out, so the node holding Caddy needs no public address and no firewall rule.

## 1. The zone <a href="#id-1-the-zone" id="id-1-the-zone"></a>

`apps.layeronecloud.com` lives under the `layeronecloud.com` zone. You need the **Zone ID** from the zone's Overview page (right-hand sidebar) and the **Account ID** from the same panel.

```
PAAS_APP_DOMAIN=apps.layeronecloud.com
PAAS_CLOUDFLARE_ZONE_ID=<32 hex chars>
PAAS_CLOUDFLARE_ACCOUNT_ID=<32 hex chars>
```

## 2. The tunnel <a href="#id-2-the-tunnel" id="id-2-the-tunnel"></a>

Zero Trust → Networks → Tunnels → **Create a tunnel** → Cloudflared. Name it for the cluster it serves, e.g. `layerone-apps-tampa`.

Cloudflare shows an install command containing a **tunnel token**. Keep that for the connector host. The **tunnel ID** is the UUID in the tunnel's URL and in its detail page.

```
PAAS_CLOUDFLARE_TUNNEL_ID=<uuid>
```

Do **not** add public hostnames in the dashboard. The platform writes a single catch-all ingress rule pointing at Caddy, and a dashboard-managed hostname list would be a second routing table that drifts from [`apps/paas/routing.py`](https://github.com/LayerOne-LLC/LayerOne/blob/main/apps/paas/routing.py).

## 3. The connector, beside Caddy <a href="#id-3-the-connector-beside-caddy" id="id-3-the-connector-beside-caddy"></a>

On the host running the Caddy edge:

```bash
cloudflared service install <TUNNEL_TOKEN>
```

Run more than one connector against the same tunnel for availability. Cloudflare load-balances across them, and losing one takes no listener down because there was never one to take down.

Then tell the platform where Caddy is, from the connector's point of view:

```
PAAS_EDGE_ORIGIN=http://127.0.0.1:80
```

Plain HTTP on purpose. TLS was terminated at Cloudflare and the connector-to- Caddy hop is local; re-terminating would mean managing a certificate for a name nobody resolves.

## 4. The API token <a href="#id-4-the-api-token" id="id-4-the-api-token"></a>

My Profile → API Tokens → **Create Token** → Create Custom Token. Three permissions, no more:

| Scope   | Permission           | Access | Why                                   |
| ------- | -------------------- | ------ | ------------------------------------- |
| Account | Cloudflare Tunnel    | Edit   | write the catch-all ingress rule      |
| Zone    | DNS                  | Edit   | one proxied CNAME per application     |
| Zone    | SSL and Certificates | Edit   | custom hostnames for customer domains |

Restrict the zone permissions to `layeronecloud.com` specifically, not "All zones". A token that can edit every zone on the account is a token that can take down the marketing site.

```
PAAS_CLOUDFLARE_API_TOKEN=<token>
```

The token is read only by the server. Nothing renders it, and the error path in `edge.py` builds its messages from Cloudflare's response rather than from the request, so it cannot appear in a log line.

## 5. Cloudflare for SaaS, for customer domains <a href="#id-5-cloudflare-for-saas-for-customer-domains" id="id-5-cloudflare-for-saas-for-customer-domains"></a>

Needed only when customers point their own domains at an app. Cloudflare will not issue a certificate for a name outside your zones without a **custom hostname**, so a customer CNAME with nothing behind it answers with a Cloudflare error rather than the app.

SSL/TLS → Custom Hostnames → enable, then set a **fallback origin**. Create a record in the zone for it first:

* Name: `edge` (i.e. `edge.apps.layeronecloud.com`)
* Type: CNAME → `<tunnel-id>.cfargotunnel.com`
* Proxy: **on**

Set that as the fallback origin. One fallback is enough for every custom hostname, which is a property of routing by `Host`: whatever name the request arrived as, Caddy matches it and picks the application. A per-hostname origin would only matter if the edge were per-application, and it is not.

Customers then CNAME their own name to their app's default hostname — `<slug>.apps.layeronecloud.com` — which is what the Domains tab already tells them, and the platform calls `ensure_custom_hostname` for the name once control is verified.

## 6. Check it <a href="#id-6-check-it" id="id-6-check-it"></a>

```bash
# From the Django shell: proves the token, account, zone and tunnel all agree.
python manage.py shell -c "
from apps.paas import edge
print(edge.edge_config().tunnel_target)
edge.ensure_tunnel_ingress(origin='http://127.0.0.1:80')
print('ingress written')
"
```

Then create an application and confirm a proxied CNAME appears in the zone pointing at `<tunnel-id>.cfargotunnel.com`.

## What this leaves you exposed to <a href="#what-this-leaves-you-exposed-to" id="what-this-leaves-you-exposed-to"></a>

Two things worth deciding about deliberately rather than discovering.

**Cloudflare becomes load-bearing.** An outage there takes every application down and there is no fallback path — you cannot repoint DNS at an origin, because there is no origin with an address. That is a different risk profile from owning the edge, and it is the price of no public addresses anywhere.

**The connector can reach every tenant network.** It is one process with a route into all of them. It runs no tenant code, holds no tenant credentials and has no inbound listener, which is the mitigation, but it is a concentration of trust and should be on a host you treat as infrastructure rather than as capacity.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.layeronecloud.com/paas/cloudflare-setup.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
