cloudflare-tunnel-subdomains
Skill0 starsChanged 9 days agoMITView on GitHub
Use when exposing a local service at a subdomain via Cloudflare Tunnel.
- Reads credentials
- Sends data out
In this repository6 other files it gives its agents.
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 35
Create at https://dash.cloudflare.com/profile/api-tokens with Zone → DNS → Edit and
- Very specific: line 57
Every API response carries "success" and "errors".
- Very specific: line 144
Always pass the token via --token-file (or the service installer), never --token <value>: the value would be visible in ps and in any …
- Distinctive: line 162
All three must pass. A healthy tunnel plus a 502 means the connector is up and the origin is not listening on the configured host:port — …
- Distinctive: line 173
Delete the DNS record first: a CNAME pointing at a deleted tunnel returns an error page for every request instead of a clean NXDOMAIN.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Publish a service at <name>.<domain> with Cloudflare Tunnel
When to Use
A local service (dev server, dashboard, container, SSH) should be reachable on the internet at a hostname you own, without opening inbound ports or owning a public IP.
The pattern: the app listens on loopback, cloudflared dials out to Cloudflare, a named
tunnel routes the hostname to http://127.0.0.1:<port>, and DNS is a proxied CNAME to
<tunnel-id>.cfargotunnel.com. No A record, no port forwarding, no certificate to manage.
Works the same on Linux, macOS and Windows; only the way you supervise the daemon differs.
0. Prerequisites
- The domain's nameservers point at Cloudflare and the zone shows Active.
- An API token, or an interactive
cloudflared login.
Token scopes (API path)
Create at https://dash.cloudflare.com/profile/api-tokens with Zone → DNS → Edit and
Account → Cloudflare Tunnel → Edit, plus Zone → Zone → Read for the id lookup.
Scope them to the one zone and account. Store it where the process can read it and no one
else can — ~/.config/cloudflare/token.env (mode 600) or %USERPROFILE%\.cloudflare\,
loaded from the environment rather than the command line.
export CF_API_TOKEN=... # never inline in a command that lands in history
export CF_ACCOUNT_ID=...
export CF_ZONE_NAME=example.comResolve the zone and account once:
ZID=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE_NAME" | jq -r '.result[0].id')
ACC=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts" | jq -r '.result[0].id')
echo "zone=$ZID account=$ACC"Every API response carries "success" and "errors". Check those, not the HTTP status
alone — Cloudflare returns 200 with success:false for some permission problems.
1. Create the tunnel
curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
--data '{"name":"<name>","config_src":"cloudflare"}' | jq -r '.result.id'config_src: "cloudflare" means the ingress rules live in the dashboard/API (remote-managed),
which is what step 2 writes. Use "local" only if you intend to ship a config.yml with the
connector; the two are not interchangeable.
2. Ingress rules
Remote-managed tunnels take the whole ingress list in one PUT — it replaces the previous config, it does not append.
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel/$TID/configurations" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
--data '{"config":{"ingress":[
{"hostname":"<name>.<domain>","path":"/api/*","service":"http://127.0.0.1:<port>"},
{"hostname":"<name>.<domain>","service":"http://127.0.0.1:<port>"},
{"service":"http_status:404"}
]}}'Rules are evaluated top to bottom. The last entry must be a catch-all with no hostname
(http_status:404, or a fallback service). Omit it and the config is rejected; put it first
and every request hits it.
Service forms: http://, https:// (add "originRequest":{"noTLSVerify":true} for a
self-signed origin), tcp://, ssh://, unix:/path/to.sock, http_status:404.
Wildcards work — "hostname":"*.<domain>" matches any single label, and needs a matching
wildcard DNS record.
Read it back:
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel/$TID/configurations" | jq .3. DNS
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
--data '{"type":"CNAME","name":"<name>","content":"'$TID'.cfargotunnel.com","proxied":true}'proxied must be true; a DNS-only CNAME to cfargotunnel.com does not resolve for
clients. An A/AAAA record for the same name shadows the CNAME — delete it first.
Interactive equivalent (no API token, uses a browser login and writes cert.pem):
cloudflared tunnel login
cloudflared tunnel create <name> # prints the tunnel id + credentials json
cloudflared tunnel route dns <name> <name>.<domain>4. Run the connector
Get the token once and keep it in a file, not in the process list:
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel/$TID/token" | jq -r '.result'Supervision, per platform:
| Platform | Foreground test | Persistent |
| Linux | cloudflared tunnel run --token-file <path> | systemd user unit (Restart=always, WantedBy=default.target) with ExecStart=cloudflared tunnel --no-autoupdate run --token-file <path> |
| macOS | same | cloudflared service install <token> (launchd), or a user LaunchAgent |
| Windows | cloudflared.exe tunnel run --token-file <path> | cloudflared.exe service install <token> installs a Windows service; run the shell as Administrator once for that step |
| Docker (all) | docker run --rm cloudflare/cloudflared:latest tunnel --no-autoupdate run --token-file /etc/cf/token | compose service with the token file mounted read-only; --network host on Linux, or point the service at host.docker.internal on Docker Desktop |
Under Docker the connector is no longer on the host's loopback: 127.0.0.1:<port> in the
ingress rule must become the host alias (host.docker.internal) or the app's container name
on a shared network.
Always pass the token via --token-file (or the service installer), never
--token <value>: the value would be visible in ps and in any monitoring that reads it.
5. Verify — three independent checks
# a) tunnel is connected
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel/$TID" | jq -r '.result.status'
# expect: healthy
# b) DNS resolves
nslookup <name>.<domain> # works on Windows, macOS, Linux; dig is not on Windows
# c) the origin answers through the edge
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://<name>.<domain>/All three must pass. A healthy tunnel plus a 502 means the connector is up and the origin is not listening on the configured host:port — check the app, not Cloudflare.
6. Teardown (in this order)
curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/$ZID/dns_records/$RECORD_ID" -H "Authorization: Bearer $CF_API_TOKEN"
curl -s -X DELETE "https://api.cloudflare.com/client/v4/accounts/$ACC/cfd_tunnel/$TID" -H "Authorization: Bearer $CF_API_TOKEN"
# then stop + disable the service/unit, and delete the token fileDelete the DNS record first: a CNAME pointing at a deleted tunnel returns an error page for every request instead of a clean NXDOMAIN.
Pitfalls
- No catch-all → the config PUT is rejected; catch-all first → every hostname 404s.
- A record instead of proxied CNAME → the hostname resolves to a Cloudflare anycast IP that has no tunnel behind it.
- Error 1033 = the hostname is routed to a tunnel that has no healthy connector. Check the
daemon,
systemctl --user status cloudflared-<name>/Get-Service cloudflared. - Error 502/521 = connector healthy, origin not reachable at the configured address.
- Redirect loop /
ERR_TOO_MANY_REDIRECTS= the app seeshttpbehind a TLS-terminating proxy and redirects again. TrustX-Forwarded-Proto, or set the app's base URL/SSL mode. - 403 for scripted requests but fine in a browser = a WAF/bot rule on the zone; a plain
curlUA is often blocked. Check Security → Events before blaming the tunnel. - Cloudflare injects scripts (email obfuscation, Rocket Loader), so byte size of the public HTML differs from local output. Compare structure, not length.
- Token files stay mode
600(icaclsthe equivalent on Windows) and out of the repo. - Rotating a tunnel token invalidates the old one immediately — update every connector that uses it in the same change.
- The zone must be active before any of this. A pending zone accepts API calls and serves nothing.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

