Skip to content

Expose a service through Cloudflare Tunnel

Run cloudflared in the cluster when you want Cloudflare Tunnel to reach a ClusterIP Service. The connector makes outbound connections to Cloudflare, so the Service does not need an Ingress or LoadBalancer.

This guide assumes you have a Cloudflare account, a domain managed by Cloudflare, and a Kix package that exposes its workload through a Service.

In the Cloudflare dashboard, open Networking > Tunnels and create a Cloudflare Tunnel. Select Add a replica and copy the token from the generated cloudflared installation command. Anyone with this token can run the tunnel, so handle it as a credential.

Store the token in a SOPS file with one top-level key:

cloudflare-token.enc.yaml
token: eyJ...

Encrypt the file before committing it. Follow Use SOPS-backed secrets if the deployment machine is not already configured to decrypt SOPS files.

Add the application package to the cluster. The example package creates Service/web in the cloudflare-example namespace:

how-to/platform/cloudflare-tunnel-cluster.nix (L21–L23)
instances.cloudflare-example.web = {
package = exposureApp;
};

View source on GitHub ↗

The Service remains internal to Kubernetes. Its default cluster address is:

http://web.cloudflare-example.svc.cluster.local:80

cluster.local comes from the cluster’s clusterDomain option. Replace the suffix if your cluster uses a different value. clusterDomain controls in-cluster DNS and is unrelated to the public domain served by the tunnel.

Create the cloudflared instance in its own namespace:

how-to/platform/cloudflare-tunnel-cluster.nix (L27–L34)
instances.tunnel-system.cloudflared = {
package = packages.cloudflared;
config = {
image = "cloudflare/cloudflared:2026.9.0";
tokenSource = ./cloudflare-token.enc.yaml;
replicas = 2;
};
};

View source on GitHub ↗

tokenSource must point to a SOPS file whose top-level token key contains the tunnel token. The example pins the connector image to a specific cloudflared release; review and update that version as part of normal dependency maintenance.

Two replicas give the tunnel more than one connector process. Both use the same remotely managed tunnel token.

Open the tunnel in the Cloudflare dashboard and add a public hostname. Set its service type to HTTP and its URL to the in-cluster address:

web.cloudflare-example.svc.cluster.local:80

This route is part of the remotely managed tunnel configuration. It is not a Kubernetes resource in the Kix cluster artifact.

Render the relevant resources before contacting the cluster:

Run in kix-examples/ Output excerpt
❱ kix build how-to-platform-cloudflare-tunnel --output json
[
  {
    "kind": "Deployment",
    "namespace": "tunnel-system",
    "name": "cloudflared",
    "replicas": 2,
    "image": "cloudflare/cloudflared:2026.9.0",
    "args": [
      "tunnel",
      "--no-autoupdate",
      "--metrics",
      "0.0.0.0:2000",
      "run"
    ]
  },
  {
    "kind": "Secret",
    "namespace": "tunnel-system",
    "name": "cloudflared-token",
    "declaredKeys": [
      "token"
    ],
    "plaintextIncluded": false
  },
  {
    "kind": "Service",
    "namespace": "cloudflare-example",
    "name": "web",
    "ports": [
      {
        "name": "http",
        "port": 80,
        "protocol": "TCP"
      }
    ]
  }
]

The excerpt shows the origin Service, the two-replica connector Deployment, and the token Secret without decrypted data.

Make the SOPS identity available and deploy:

Run in kix-examples/
❱ export SOPS_AGE_KEY_FILE=/secure/path/age-key.txt
❱ kix deploy how-to-platform-cloudflare-tunnel

Check the connector pods and the tunnel status:

❱ kubectl get pods -n tunnel-system

Cloudflare should show the tunnel as healthy once at least one connector is registered. Test the public hostname from outside the cluster:

❱ curl https://app.example.com

Replace app.example.com with the hostname you configured.

Inspect the connector logs when the tunnel remains down or requests return an origin error:

Run in kix-examples/
❱ kix logs how-to-platform-cloudflare-tunnel cloudflared --since 10m

Check these common causes:

  • The SOPS identity cannot decrypt cloudflare-token.enc.yaml.
  • The token belongs to another tunnel or has been rotated.
  • The public hostname route uses the wrong namespace, Service name, port, or clusterDomain suffix.
  • Network policy blocks DNS or outbound connections from tunnel-system.

When you rotate the tunnel token, replace the value in the encrypted file and deploy again. Restart the connector pods after the Secret update so each process reads the new token.