Skip to content

About

Kubernetes Gateway API implementation for Cloudflare Tunnel. HTTPRoute and GRPCRoute are served by an in-cluster L7 proxy with the cloudflared transport built in. Gateway API conformant.

Topics

Resources

Contributing

Security policy

Stars

36 stars

Watchers

1 watching

Forks

Latest commit

 

History

646 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cloudflare Tunnel Gateway Controller

Go Version License Release CI Docs Gateway API Conformance

Kubernetes controller implementing Gateway API for Cloudflare Tunnel.

Expose in-cluster services through a Cloudflare Tunnel using standard Gateway API resources (Gateway, HTTPRoute, GRPCRoute), with no public load balancer or inbound firewall rule.

Features

  • Standard Gateway API: GatewayClass, Gateway, HTTPRoute, GRPCRoute, ListenerSet
  • In-process L7 reverse proxy embeds the cloudflared transport — a single data plane handles routing and tunnel egress
  • Hot reload of routing configuration (no cloudflared restart on route changes)
  • Path matching (prefix, exact, regex), header / query-parameter / HTTP-method matching
  • Request and response header modification, URL rewrite, request redirect, request mirror
  • Weighted traffic splitting across backends
  • HTTPRoute CORS filter
  • Cross-namespace backend references gated by ReferenceGrant
  • Backend TLS (BackendTLSPolicy) and backend WebSocket via appProtocol
  • Multi-tenant isolation: per-namespace hostname-ownership enforcement (admission policy + controller), route-collision detection, and optional per-Gateway data planes (a dedicated proxy and tunnel per Gateway, where Cloudflare must confirm a Gateway's connector token before its tunnel is written to, a Gateway claiming a tunnel another namespace already serves is refused, and an operator can cap how many dedicated data planes one namespace may run)
  • Request-level Prometheus metrics from the proxy data plane (per-hostname rates, latency, in-flight gauge for autoscaling)
  • Leader election for high-availability deployments
  • Multi-arch images (amd64, arm64), signed with cosign

Warning: The controller assumes exclusive ownership of the tunnel configuration. It removes any ingress rules not managed by HTTPRoute/GRPCRoute resources. Do not point it at a tunnel that has manually configured routes or is shared with other systems.

L7 Proxy

The controller runs an in-process L7 reverse proxy inside the cloudflared process via the OverrideProxy hook (using a fork of cloudflared). All tunnel traffic is intercepted by the proxy, which applies Gateway API routing rules — hostname, path, header, query and method matching, filters, and weighted backend selection — before forwarding to backends. The tunnel ingress document the controller writes through the Cloudflare API only feeds the Cloudflare dashboard; request routing is handled entirely by the proxy, so HTTPRoute features work end-to-end regardless of Cloudflare Tunnel's native capabilities.

See L7 Proxy Architecture for full details.

If the tunnel's initial connection to the Cloudflare edge fails (for example, cluster DNS not yet reachable right after a node reboot), the proxy retries with backoff and reports NotReady instead of restarting — see Proxy Pod Stuck NotReady After a Restart.

Quick Start

# 1. Install Gateway API CRDs
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.2/standard-install.yaml

# 2. Create credentials Secrets
kubectl create namespace cloudflare-tunnel-system
kubectl create secret generic cloudflare-credentials \
  --namespace cloudflare-tunnel-system \
  --from-literal=api-token="YOUR_API_TOKEN"
kubectl create secret generic cloudflare-tunnel-token \
  --namespace cloudflare-tunnel-system \
  --from-literal=tunnel-token="YOUR_TUNNEL_TOKEN"

# 3. Install the controller
helm install cloudflare-tunnel-gateway-controller \
  oci://ghcr.io/lexfrei/charts/cloudflare-tunnel-gateway-controller \
  --namespace cloudflare-tunnel-system \
  --set gatewayClassConfig.create=true \
  --set gatewayClassConfig.tunnelID=YOUR_TUNNEL_ID \
  --set gatewayClassConfig.cloudflareCredentialsSecretRef.name=cloudflare-credentials \
  --set proxy.tunnelTokenSecretRef.name=cloudflare-tunnel-token

# 4. Create HTTPRoute to expose your service
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
spec:
  parentRefs:
    - name: cloudflare-tunnel
      namespace: cloudflare-tunnel-system
  hostnames:
    - app.example.com
  rules:
    - backendRefs:
        - name: my-service
          port: 80
EOF

See Installation for detailed setup instructions.

Compatibility

Component Supported
Kubernetes 1.31+
Gateway API CRDs Standard channel (Gateway API v1.6.2)

The prerequisites page explains where each bound comes from.

The conformance badge names the report accepted upstream: release v3.3.1, run with conformance suite v1.6.1. It moves when a newer report is accepted, not when the Gateway API version above changes.

Prerequisites

  • Kubernetes cluster with Gateway API CRDs installed
  • Cloudflare account with a pre-created Cloudflare Tunnel
  • Cloudflare API token with tunnel permissions

Create Cloudflare Tunnel

Before deploying the controller, you must create a Cloudflare Tunnel:

  1. Go to Cloudflare Zero Trust Dashboard
  2. Navigate to Networks > Tunnels
  3. Click Create a tunnel
  4. Choose Cloudflared connector type
  5. Name your tunnel and save the Tunnel ID and Tunnel Token

The controller manages tunnel ingress configuration via the Cloudflare API. Tunnel traffic is terminated by the in-process L7 proxy that ships with the chart; supply the tunnel token via proxy.tunnelTokenSecretRef in Helm values.

Cloudflare API Token Permissions

Create an API token at Cloudflare API Tokens with the following permissions:

Scope Permission Access
Account Cloudflare Tunnel Edit

Account ID is auto-detected from the API token when not explicitly provided (works if the token has access to a single account).

Installation

Helm is the only supported installation method. It handles CRD installation, RBAC setup, and provides a simple upgrade path.

helm install cloudflare-tunnel-gateway-controller \
  oci://ghcr.io/lexfrei/charts/cloudflare-tunnel-gateway-controller \
  --namespace cloudflare-tunnel-system \
  --create-namespace \
  --values values.yaml

See charts/cloudflare-tunnel-gateway-controller/README.md for all configuration options.

For manual installation without Helm, see Manual Installation.

Usage

Create standard Gateway API HTTPRoute or GRPCRoute resources referencing the cloudflare-tunnel Gateway. The controller syncs routes to the in-process L7 proxy and the Cloudflare Tunnel configuration with hot reload (no cloudflared restart). Both HTTPRoute and GRPCRoute are served by the proxy at runtime.

Supported Gateway Fields

Field Supported Notes
spec.gatewayClassName ✅ Must match controller's GatewayClass
spec.listeners ✅ Fully processed for route binding and status
spec.listeners[].name ✅ Used for route binding, status reporting, attached route counting
spec.listeners[].protocol ✅ HTTP/HTTPS listeners bind HTTPRoute and GRPCRoute
spec.listeners[].port ✅ Used for route binding when route specifies a port, and as the redirect port of a scheme-less RequestRedirect (redirect port)
spec.listeners[].hostname ✅ Routes must have intersecting hostnames; a host is served only by routes on the most specific listener that matches it (listener isolation)
spec.listeners[].tls ✅ certificateRefs validated, with ReferenceGrant support, but never served: clients get the Cloudflare edge certificate
spec.listeners[].allowedRoutes ✅ Namespace (Same/All/Selector) and kind filtering
spec.tls.frontend ❌ Refused: the Gateway is Accepted=False and its routes are not served; validate client certificates at the Cloudflare edge instead (details)
spec.addresses ❌ Only the tunnel CNAME is served: another hostname leaves the Gateway Programmed=False, any other address type refuses it (Accepted=False) (details)
spec.infrastructure.parametersRef ✅ Opts the Gateway into a dedicated data plane (GatewayConfig, group cf.k8s.lex.la) — its own proxy and tunnel
spec.infrastructure.labels / .annotations ✅ Propagated to the rendered per-Gateway resources, generated Secrets and pod template

Note: Cloudflare Tunnel terminates TLS at its edge. HTTPS listeners are accepted and served, and their certificate references are validated (including cross-namespace ReferenceGrant checks), but the client gets the Cloudflare edge certificate, not the listener's certificateRefs.

Supported Route Fields

All matching and filter behavior is performed by the in-process L7 proxy that the chart deploys alongside the controller.

HTTPRoute:

Field Supported Notes
spec.hostnames ✅ Wildcard * supported
spec.rules[].matches[].path ✅ PathPrefix, Exact, RegularExpression
spec.rules[].matches[].headers ✅ Exact and RegularExpression
spec.rules[].matches[].queryParams ✅ Exact and RegularExpression
spec.rules[].matches[].method ✅ All HTTP methods
spec.rules[].filters ✅ Header modifier, redirect, URL rewrite, mirror, CORS
spec.rules[].backendRefs ✅ Service name, namespace, port
spec.rules[].backendRefs[].namespace ✅ Cross-namespace refs require ReferenceGrant
spec.rules[].backendRefs[].weight ✅ True weighted traffic splitting across backends
spec.rules[].retry ✅ Experimental channel; see Retries

GRPCRoute: ✅ Served by the in-process L7 proxy. gRPC service/method matches map onto /{service}/{method} path rules. The upstream hop is cleartext h2c by default; attaching a BackendTLSPolicy upgrades the hop to TLS with HTTP/2 negotiated via ALPN, and the Gateway's clientCertificateRef is presented on the handshake for mTLS. See GRPCRoute docs.

A backendRef may target a core Service, a ServiceImport (multicluster.x-k8s.io), or an ExternalBackend (cf.k8s.lex.la, an out-of-cluster HTTP(S) URL). Other kinds are reported ResolvedRefs=False, InvalidKind.

Limitations

The L7 proxy handles routing for every tunnel request, so most Gateway API behavior works end-to-end. The caveats that remain are documented in full on the Limitations page:

  • Edge-side constraints — Cloudflare hostname registration and edge HTTPS termination apply to all traffic: an HTTPS listener is served, but clients get the edge certificate, not the listener's certificateRefs.
  • A Gateway that sets spec.tls.frontend (client certificate validation) is refused, because clients complete TLS with the Cloudflare edge; require client certificates at the edge instead.
  • spec.addresses can only name the tunnel CNAME: another hostname leaves the Gateway Programmed=False with reason AddressNotUsable, and any other address type refuses it with reason UnsupportedAddress.
  • gRPC requires Cloudflare zone gRPC proxying enabled (dashboard → Network → gRPC); otherwise the edge returns 403 zone-wide for application/grpc.
  • BackendTLSPolicy (proxy → backend TLS) is supported at minimum-viable scope: core Service targets only, explicit CACertificateRefs only, Hostname and URI SANs, backend mTLS via the Gateway's clientCertificateRef. A ServiceImport backend is dialed plaintext.
  • Backend WebSocket via appProtocol: kubernetes.io/ws (and /wss with a BackendTLSPolicy).
  • timeouts.request / timeouts.backendRequest are enforced as header-only deadlines, so streaming responses (SSE, chunked, gRPC server-streaming) keep flowing past the deadline.
  • retry (Experimental channel) retries every method at most 10 times, at least 10ms apart, and a request body over 64 KiB, or a streamed one the first attempt did not finish sending, is not resent once the proxy has started reading it.
  • Unavailable backends in a weighted rule return a status (500/503, gRPC UNAVAILABLE) for their share rather than dialing a dead address, so the other backends keep serving.
  • A match pattern the proxy cannot compile drops its own rule, reported on the route that carries it; other rules and other routes keep serving.
  • RequestMirror copies are dropped once a filter is at its in-flight dispatch cap (proxy.mirror.maxInFlight, counted by cftunnel_proxy_mirror_dropped_total), so a mirror backend that stops answering cannot grow the proxy's memory with request rate.
  • Informational 1xx responses such as 103 Early Hints are not forwarded through the tunnel; the client gets only the final response.
  • HTTPRouteRule.name uniqueness is not enforced at admission; an opt-in ValidatingAdmissionPolicy (ruleNameUniquenessPolicy Helm value) enforces it.
  • The non-canonical group: core is accepted for backendRefs and BackendTLSPolicy CA refs, and rejected for a Gateway's clientCertificateRef, listener certificateRefs and the ReferenceGrant authorising them. Write group: "", the spelling the Gateway API defines, and the asymmetry cannot bite.
  • Knative Serving via net-gateway-api needs a split-horizon setup — see the Knative Serving guide — because its readiness prober dials the Gateway's tunnel address directly, which is not reachable in-cluster.

The proxy can emit a structured per-request access log via proxy.accessLog.enabled: true. See Access Logging.

See the Gateway API documentation for full details and examples.

External-DNS Integration

The controller sets status.addresses on the Gateway with the tunnel CNAME (TUNNEL_ID.cfargotunnel.com). If you run external-dns with the Gateway API source, it will automatically create DNS records for your HTTPRoute hostnames.

All external-dns annotations (TTL, provider-specific settings, etc.) should be placed on HTTPRoute resources, not on the Gateway. See the external-dns Gateway API documentation for details.

A Gateway with its own data plane keeps that address while its configuration is broken, so external-dns does not withdraw the record on a configuration error. The address is what records which tunnel the plane holds, and clearing it would surrender that tunnel. Gateways on the shared plane clear it as before.

FAQ

Why do I get SSL certificate errors for multi-level subdomains?

Cloudflare's free Universal SSL certificates only cover root and first-level subdomains:

  • ✅ example.com, *.example.com
  • ❌ app.dev.example.com

For multi-level subdomains, you need Advanced Certificate Manager or a Business/Enterprise plan.

Documentation

Full documentation is available at cf.k8s.lex.la.

Section Description
Getting Started Prerequisites, installation, and quick start
Configuration Controller configuration and Helm values
Gateway API Supported resources, examples, and limitations
Guides L7 proxy setup, external-dns, cross-namespace, monitoring
Operations Troubleshooting, metrics, manual installation
Development Development setup, architecture, contributing
Reference CRD reference, Helm chart, security policy

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for guidelines.

Common development tasks are available via make:

make help        # list all targets
make check-deps  # verify required tools are installed
make test        # run tests
make lint        # run linter

Security

For security issues, please see SECURITY.md.

The proxy's config API (where the controller pushes routing changes) is authenticated, served over TLS with certificates the controller issues itself, and network-restricted by default — see the Security reference.

For multi-tenant deployments — isolation boundaries, hostname-ownership enforcement, and per-Gateway data planes — see the Multi-Tenancy guide.

License

BSD 3-Clause License - see LICENSE for details.

About

Kubernetes Gateway API implementation for Cloudflare Tunnel. HTTPRoute and GRPCRoute are served by an in-cluster L7 proxy with the cloudflared transport built in. Gateway API conformant.

Topics

Resources

Contributing

Security policy

Stars

36 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages