Cloudflare Tunnel and Access
Cloudflare Tunnel publishes a self-hosted service on a Cloudflare hostname without a public IP or an open inbound port: the cloudflared daemon dials outbound to Cloudflare's edge and requests come back down those connections. Cloudflare Access sits in front of a hostname and makes each visitor sign in with an identity provider before a request reaches the tunnel, then passes a signed JWT to the origin. cloudflared is Apache 2.0; Tunnel and Access are usable on the Zero Trust Free plan (up to 50 users). It fits homelabs and small teams that self-host behind NAT and want admin pages behind single sign-on.
Cloudflare Tunnel connects a service on a private network to Cloudflare's edge so the service is reachable on a public hostname with no public IP, no port forwarding and no inbound firewall rule. Cloudflare Access adds an identity check in front of that hostname, so admin consoles and dashboards are only reachable after a single sign-on login. The official docs at developers.cloudflare.com/cloudflare-one own the dashboard steps, which Cloudflare reorganizes often.
What it is
Two products from the Cloudflare One (Zero Trust) family that are often used together:
- Cloudflare Tunnel is the connectivity part. A small daemon,
cloudflared, runs next to the origin and holds outbound connections to Cloudflare. A tunnel is a persistent logical object with a UUID; one or morecloudflaredprocesses (connectors, also called replicas) attach to it. Hostnames point at the tunnel through a DNS CNAME to<UUID>.cfargotunnel.com. - Cloudflare Access is the authorization part. An Access application covers a hostname (or a hostname and path). Its policies decide who gets in, based on identity (email, email domain, identity-provider group), service tokens, or non-identity signals such as IP range and country.
Licence and cost. cloudflared is open source under Apache 2.0 at github.com/cloudflare/cloudflared and ships as standalone binaries, a Docker image, and Debian, RPM and Homebrew packages. The edge service is Cloudflare's and is not open source. Cloudflare lists the Zero Trust Free plan at $0 for teams under 50 users, which covers a homelab or a small company's admin surface.
Release line. cloudflared uses calendar versions (YYYY.M.patch) and ships several releases a month; 2026.10.1 is the latest at the time of writing. Cloudflare supports versions within one year of the most recent release, and removal of a CLI flag, environment variable, config key or command is announced in the Tunnel changelog before the release that removes it. Two recent releases carry warnings in the release notes: 2026.8.0 strips trailing slashes from requests to HTTP origins (which can cause redirect loops) and 2026.8.1 normalizes paths (which can break apps that need encoded URLs). Both notes say to run 2026.8.2 or later.
How it works
On start, each cloudflared process authenticates with the tunnel's credentials and opens four outbound connections to four servers in at least two Cloudflare data centers, on port 7844 (UDP for QUIC, TCP for HTTP/2). Nothing listens on the origin side. When a request arrives at the edge for a hostname routed to the tunnel, Cloudflare sends it down one of those existing connections. cloudflared matches the hostname (and optional path) against its ingress rules, top to bottom, and proxies the request to the first matching service URL, for example http://web.default.svc:80.
If the hostname is also an Access application, the edge checks for a valid CF_Authorization cookie first. Without one, the visitor is sent to the Access login page, signs in with a configured identity provider, and the policies are evaluated. On success, Access issues a signed JWT, and every proxied request carries it in the Cf-Access-Jwt-Assertion header, so the origin (or cloudflared itself) can verify that the request passed Access.
Step 1 of 3. cloudflared presents the tunnel token and opens four outbound connections on port 7844 to at least two data centers.
The connector dials out first; public and admin requests then ride those outbound connections.
There are two ways to manage a tunnel. A remotely managed tunnel is created in the dashboard or API; cloudflared runs with only a token, and routes ("published applications") are edited in Cloudflare. A locally managed tunnel is created with the CLI and reads its ingress rules from a config.yml next to the daemon, which suits keeping the routes in Git. Extra replicas each add four connections to the same tunnel. Replicas give failover and zero-downtime config changes; they do not load-balance. Cloudflare sends each request to the geographically closest replica and retries another if that connection fails.
When to use it
Reach for it when:
- The origin sits behind NAT or a home router with no public IP, or opening inbound ports is not allowed.
- You want admin consoles behind SSO (Rancher, Keycloak's admin console, Grafana, Argo CD) without running a VPN client on every device.
- The DNS zone is already on Cloudflare; routes and Access policies then live next to the DNS records.
- You want DDoS absorption and edge TLS for free in front of a small self-hosted site.
Skip it when:
- The zone cannot move to Cloudflare DNS. Published hostnames need a website added to Cloudflare.
- Traffic must not be decrypted by a third party. TLS for the public hostname terminates at Cloudflare's edge.
- Uploads are larger than the plan's request body limit (100 MB on Free) and cannot be chunked.
At a glance
Core concepts
- Tunnel — the persistent object, identified by a UUID, that hostnames and private-network routes point at.
- Connector / replica — one running
cloudflaredprocess attached to a tunnel. Up to 25 per tunnel. - Ingress rule — hostname (and optional path) to service URL. The last rule must be a catch-all, usually
http_status:404. - Published application — a public hostname routed to a tunnel. Without Access in front, anyone on the internet can reach it.
- Access application — a hostname under Access control, with an AUD tag that identifies it in issued JWTs.
- Policy — Allow, Block, Bypass or Service Auth, built from Include (OR), Require (AND) and Exclude (NOT) rules.
- Service token — a client ID and secret pair that lets a script or CI job pass Access with the Service Auth action.
Key facts
| Item | Value |
|---|---|
| Outbound port | 7844, UDP (QUIC) and TCP (HTTP/2) |
| Required hostnames | region1.v2.argotunnel.com, region2.v2.argotunnel.com |
| Connections per connector | 4, to at least 2 data centers |
| Replicas per tunnel | 25 |
| Tunnels per account | 1,000 |
| Access applications per account | 500 |
| Zero Trust Free plan | Up to 50 users |
| Health endpoint | /ready on the metrics port, 200 only with an active edge connection |
| Supported versions | Within one year of the latest release |
Cloudflare Tunnel vs an open inbound port
| Aspect | Cloudflare Tunnel | Port forward + reverse proxy |
|---|---|---|
| Inbound firewall | Closed; outbound 7844 only | Ports 80/443 open to the internet |
| Public IP | Not needed | Needed (static or dynamic DNS) |
| TLS certificate | Edge certificate managed by Cloudflare | Origin manages its own (cert-manager, ACME) |
| SSO in front | Access policies at the edge | Separate auth proxy at the origin |
| Who sees plaintext | Cloudflare's edge | Only the origin |
| Dependency | Cloudflare account and DNS | Router, ISP and origin only |
How to integrate
Remotely managed tunnel in Kubernetes
Create the tunnel in the Zero Trust dashboard (Networking > Tunnels), copy its token, and store it as a Secret. The Deployment follows the official Kubernetes guide, with the image pinned rather than latest:
apiVersion: v1
kind: Secret
metadata:
name: tunnel-token
stringData:
token: <YOUR_TUNNEL_TOKEN>
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
spec:
replicas: 2 # high availability only; no load balancing
selector:
matchLabels: {pod: cloudflared}
template:
metadata:
labels: {pod: cloudflared}
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:2026.9.3
command: [cloudflared, tunnel, --no-autoupdate, --loglevel, info,
--metrics, 0.0.0.0:2000, run]
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef: {name: tunnel-token, key: token}
livenessProbe:
httpGet: {path: /ready, port: 2000}
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 1
Then add a published application route on the tunnel's Routes tab, for example app.example.com to http://app.default.svc.cluster.local:80.
Locally managed tunnel from the CLI
cloudflared tunnel login # authorizes a zone, writes cert.pem
cloudflared tunnel create homelab # writes <UUID>.json credentials
cloudflared tunnel route dns homelab app.example.com
cloudflared tunnel ingress validate # checks config.yml
cloudflared tunnel ingress rule https://app.example.com # shows the matching rule
cloudflared tunnel run homelab
# config.yml
tunnel: <Tunnel-UUID>
credentials-file: /etc/cloudflared/<Tunnel-UUID>.json
ingress:
- hostname: app.example.com
service: http://localhost:8000
- hostname: admin.example.com
service: https://localhost:8443
originRequest:
originServerName: admin.example.com # name on the origin's certificate
access:
required: true
teamName: <your-team-name>
audTag: [<AUD-tag>]
- service: http_status:404 # catch-all, required
Access in front of an admin host
- In Zero Trust, add an identity provider (one-time PIN works with no setup; OIDC for Keycloak, Google or GitHub).
- Add a self-hosted Access application for
admin.example.com. - Add one Allow policy with Include > Emails set to the permitted address. Do not include Everyone or a bare login method.
- Copy the application's AUD tag into the origin's JWT check (or the
accessblock above). - For scripts, create a service token and a Service Auth policy; the client sends
CF-Access-Client-IdandCF-Access-Client-Secretheaders.
An origin that validates the JWT itself fetches keys from https://<team>.cloudflareaccess.com/cdn-cgi/access/certs, matches the token's kid, and checks the RS256 signature, the issuer and the aud claim.
In the GL stack
cloudflared 2026.9.3 runs on the prod cluster and publishes the product sites and their APIs, such as builddaily.io and paiddaily.io, over outbound-only connections. The Mac that hosts the clusters opens no inbound ports, so all traffic from the internet arrives through the tunnel.
- Admin hosts. Cloudflare Access gates the admin hostnames, the Rancher UI and the Keycloak master realm (the realm that administers Keycloak itself), to one identity.
- Two layers of login. An admin request passes Access first and then the application's own login, so an application password alone does not reach the console.
- Plan. Everything runs on the Free plans of Cloudflare and Cloudflare Zero Trust.
Gotchas
- The catch-all rule is mandatory. A
config.ymlwith ingress rules must end with a rule that has no hostname.cloudflared tunnel ingress validatecatches a missing one before a deploy. - Paths are not rewritten. A route with a path sends matching requests to the service URL but does not strip the path; the origin must serve under that prefix.
- Deep subdomains fail TLS. Universal SSL covers one level of subdomain.
api.beta.example.comfails with a handshake error (ERR_SSL_VERSION_OR_CIPHER_MISMATCH); keep published hostnames one level deep, such asapi-beta.example.com. - 502 versus 1033. A 502 means the tunnel is up but
cloudflaredcannot reach the origin (wrong port,httpversushttps). A 1033 means no connector is connected, or the CNAME points at the wrong tunnel. - HTTPS origins and certificates. An origin with a private CA needs
caPool; a certificate for a different name needsoriginServerName.noTLSVerify: trueis a temporary last resort. - Do not autoscale replicas. Scaling down drops the user connections on the removed replica. Run a fixed count, two or more.
- Avoid 2026.8.0 and 2026.8.1. The release notes flag both for path changes that break some HTTP origins. Pin a version and read the notes before bumping.
- Firewalls need both protocols. The
quictransport uses UDP 7844 andhttp2uses TCP 7844. The firewall docs say to allow both. - Bypass is unlogged. A Bypass policy turns off Access enforcement and logging for matching traffic. Use it for health-check paths only.
- One-time PIN plus a broad Include is open. Including the one-time PIN login method as the rule lets any valid email address in. Name the emails.
Risks
- Vendor lock-in. Hostnames, routes and Access policies are Cloudflare configuration. Moving off means new DNS, new certificates and a new auth layer at once.
- Single point of dependency. A Cloudflare outage or an account suspension takes every published hostname offline, even with the origin healthy.
- TLS terminates at the edge. Cloudflare sees request contents in plaintext. Sensitive data needs end-to-end encryption at the application layer.
- Token custody. A tunnel token lets anyone run a connector for that tunnel and receive its traffic. Store it in a secret manager and rotate it if it leaks.
- Free-plan terms can change. The 50-user free tier is a pricing decision, not a contract. Keep a written note of what a move to self-hosted ingress would take.
- Project health is strong.
cloudflaredships several releases a month under Apache 2.0, with a published one-year support window.
Alternatives
- Tailscale Funnel — when the machines are already on a Tailscale tailnet and the zone does not need to be on Cloudflare.
- ngrok — for short-lived demos and webhook testing rather than a permanent production ingress.
- frp (fast reverse proxy) — when the relay must be self-hosted on a rented VPS, so no third party terminates TLS.
- Open port with Traefik, cert-manager and an auth proxy — when a static public IP exists and full control of TLS outweighs the exposed port.
Related
- Traefik and cert-manager — in-cluster routing and certificates behind the tunnel.
- Keycloak and Rancher — the admin hosts that Access gates.
- K3s and Lima — the clusters and VMs that
cloudflaredruns on. - OpenBao and External Secrets — where tunnel tokens belong.
- Official: Cloudflare Tunnel docs, cloudflared releases, Access policies, Validating the Access JWT.
