The situation
An application listens on 10.0.1.20:8000 — a NAS, a VM, an appliance, anything. You do
not want to install software on it, and you will not open a port on the router. A second Debian machine
on the same subnet, 10.0.1.10, will act as the connector.
Which one
| A — cloudflared | B — Mesh | |
|---|---|---|
| You reach it at | https://app.example.com | 10.0.1.20:8000 |
| Works from | any browser, anywhere | enrolled devices only |
| Client software | none | Cloudflare One client required |
| Protocols | HTTP, plus SSH/RDP via client | any TCP, UDP, ICMP |
| Authorisation | Access application + policy | Gateway network policy |
| Public DNS | yes — a proxied record | none |
| Share with a guest | add their email to the policy | they must enrol a device |
| Pick it when | it's a web app and convenience matters | it's unauthenticated, or not HTTP |
The connector machine must already reach the app across the LAN. Confirm from
10.0.1.10 before configuring anything in Cloudflare — a tunnel cannot fix a route or
firewall that is broken locally:
curl -sI http://10.0.1.20:8000 | head -1
nc -vz 10.0.1.20 8000
Publish it with cloudflared
Public hostname, protected by Cloudflare Access. Reachable from any browser.
-
Create the tunnel
one.dash.cloudflare.com › Networks › Tunnels › Create a tunnel › Cloudflared
Name it after the connector machine —
lan-connector, not after the app. One tunnel can publish many services on the subnet. Save, then leave the token screen open. -
Install the connector on 10.0.1.10
sudo mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' \ | sudo tee /etc/apt/sources.list.d/cloudflared.list sudo apt-get update && sudo apt-get install -y cloudflared sudo cloudflared service install <YOUR_TUNNEL_TOKEN>The suite is literally
anyfor every Debian and Ubuntu release. Confirm it came up:systemctl status cloudflared journalctl -u cloudflared -n 30 --no-pager # expect "Registered tunnel connection"The tunnel should read Healthy in the dashboard before you continue.
-
Create the Access application — before the hostname exists
Zero Trust › Access › Applications › Add an application › Self-hosted
Field Value Name LAN appSession duration 24 hoursSubdomain app— never leave this blankDomain example.comPath empty Identity providers untick Accept all, select only yours Then add a policy: action
Allow, Include →Emails→ your address. Finish the wizard and confirm the app is listed. A policy that is created but never attached protects nothing.Order is not cosmeticThe next step makes the app reachable from the entire internet. If the Access application does not exist when DNS starts resolving, there is nothing between strangers and
10.0.1.20:8000. Build the wall first, then open the road. -
Point a public hostname at the LAN address
Networks › Tunnels › your tunnel › Edit › Public Hostname › Add a public hostname
Field Value Subdomain app— must match the Access application exactlyDomain example.comType HTTPURL 10.0.1.20:8000This is the only line that differs from a same-machine setup. The URL is resolved by the connector, not by Cloudflare, so a private address is perfectly valid here — it simply has to be reachable from
10.0.1.10. Saving creates the proxiedCNAMEautomatically.If it misbehaves
- App serves HTTPS with a self-signed certificate → set Type to
HTTPSand enable No TLS Verify. - Wrong site, or a redirect loop → set HTTP Host Header to
app.example.com. - Slow to start → raise Connect timeout.
- App serves HTTPS with a self-signed certificate → set Type to
-
Verify from outside
curl -sI https://app.example.com | head -1 # want: HTTP/2 302Result Meaning 302 Correct — Access is challenging. Redirect goes to cloudflareaccess.com.200 No authentication happened. Either unprotected, or you are testing from a WARP-enrolled device. 502 Access passed you through but the connector cannot reach 10.0.1.20:8000.530 Tunnel is down — check the service on 10.0.1.10.Do not trust a browser on a WARP deviceIf your machine runs the Cloudflare One client enrolled in the same organisation, Access may authenticate it silently from device identity — no login appears. Incognito does not help, because the request still leaves through the same tunnel. Test on a phone with mobile data and WARP off; that is the only reading that means anything.
Reach it privately with Cloudflare Mesh
No hostname, no public DNS. The app keeps its own address and only enrolled devices can route to it.
The node receives a Mesh IP from 100.96.0.0/12, but the app does not. You
advertise 10.0.1.0/24 as a route behind the node, so from an enrolled laptop you open
http://10.0.1.20:8000 — the real address, unchanged — as though you were sitting
on that LAN.
-
Create the node
Cloudflare dashboard › Networking › Mesh › Add a node
Name it
lan-nodeand select Create node. Copy the connector token from the wizard — it is single-use, and retrieving it later means returning to the node's detail page. -
Enable IP forwarding on 10.0.1.10
Required before connecting, since this node routes for a subnet rather than only itself:
printf 'net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\nnet.ipv6.conf.all.accept_ra = 2\n' \ | sudo tee /etc/sysctl.d/99-zzz-cloudflare-warp-connector.conf sudo sysctl --system -
Install the Cloudflare One client
A different package and repository from
cloudflared:curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg \ | sudo gpg --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ noble main" \ | sudo tee /etc/apt/sources.list.d/cloudflare-client.list sudo apt-get update && sudo apt-get install -y cloudflare-warpDo not use$(lsb_release -cs)hereUnlike the
cloudflaredrepo, this one has noanysuite — and Cloudflare publishes only to LTS and Debian stable names. On an interim release such as Ubuntu 25.04 (plucky) or 24.10 (oracular), the generated line 404s on the Release file. Pin an available suite by hand:noble,jammy,focal,bookworm,trixie,bullseye. -
Join the mesh, headless
sudo warp-cli --accept-tos connector new <CONNECTOR_TOKEN> sudo warp-cli --accept-tos connect warp-cli --accept-tos statusNo interactive login occurs — the token authenticates the machine. The node then appears in the Mesh list with its assigned Mesh IP.
-
Advertise the subnet
On the node's detail page, add a CIDR route for
10.0.1.0/24. This is the step that makes10.0.1.20reachable — without it, only the node itself answers.Advertise the narrowest range that covers what you need.
10.0.1.0/24exposes the whole subnet to anyone your Gateway policy allows; a/32for a single host is tighter if only this one app matters.Which addresses change, and which don't
No translation is applied to an advertised CIDR. The node acts as a gateway: traffic for
10.0.1.0/24is forwarded to it, and it delivers to the right host with addressing intact — which is exactly why step 2 enables IP forwarding.Receives a Mesh IP ( 100.96.0.0/12)Keeps its own address The node itself, 10.0.1.10Every host behind it on the advertised subnet Each enrolled client device 10.0.1.20— the application machineHostname routes are the exceptionIf you use a hostname route rather than a CIDR route, Gateway hands the client a token IP from
172.64.128.0/20and rewrites the destination to the real private address before the packet reaches the node. Seeing a172.64.x.xaddress on the client is expected there, not a misconfiguration — it is a transient routing mechanism rather than persistent translation. CIDR routes never do this. -
Configure the devices that will connect
Zero Trust › Settings › WARP Client › Profile settings
- Protocol must be MASQUE. On WireGuard you silently lose hostname routes, IPv6 CIDR routes and high availability — nothing errors, the features just don't work.
- Split Tunnel in Include mode, listing
100.96.0.0/12and10.0.1.0/24. Include mode leaves the rest of the device's traffic alone.
Install the Cloudflare One client on each laptop or phone, choose Zero Trust security, enter your team name, and authenticate.
Overlapping networksA device physically on
10.0.1.0/24must not route that range through the mesh — it would tunnel traffic back to the network it is already sitting on. Mesh access is for devices away from the LAN; at home they reach the app directly. -
Restrict who can reach it
Zero Trust › Networks › Policies › Add a policy
Enrolment alone is authentication, not authorisation — by default any enrolled device can reach an advertised route. Add a network policy scoped to destination IP
10.0.1.20and port8000, matched on the identities or device posture you intend to allow, and a lower-precedence Block for everything else. -
Verify from an enrolled device
nc -vz 10.0.1.20 8000 curl -sI http://10.0.1.20:8000 | head -1 ping 10.0.1.20 # ICMP works here; it would not through a tunnelDone whenAn enrolled laptop off the LAN reaches
http://10.0.1.20:8000, and the same laptop with the client disconnected cannot. Confirm both directions — the second is the one that proves the app is private.
Running both
The two routes coexist on one machine. cloudflared and cloudflare-warp are
separate daemons with separate repositories, and neither is aware of the other. Publishing a web UI
over a hostname while reaching its database privately over the mesh is a reasonable arrangement.
What you must not do is assume one protects the other. An Access policy on
app.example.com places no restriction whatsoever on 10.0.1.20:8000 over the
mesh, and a Gateway network policy does nothing for the published hostname. They are independent
control surfaces, and each needs its own rule.
Common failures
| Symptom | Cause |
|---|---|
| 502 via hostname | Connector cannot reach 10.0.1.20:8000. Test with curl from 10.0.1.10 — usually a host firewall on the app machine, or the app bound to loopback only. |
| App bound to 127.0.0.1 | Then 10.0.1.20:8000 is unreachable from anywhere, including the connector. Rebind it to the LAN address, or run the connector on the app's own machine. |
| 404 on apt update | WARP repo suite doesn't exist for your release. Pin noble or bookworm. |
| Mesh node connects, app unreachable | No CIDR route advertised, or IP forwarding not enabled. Both are required. |
| Nothing routes on the client | Split Tunnel in Exclude mode, or the subnet missing from the Include list. |
| Hostname returns 200 with no login | Testing from a WARP-enrolled device. Retest off-network before concluding anything. |