Skip to content
Install the CLI

How-to guide Platform capabilities

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
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, beside the Secret that holds its token:

how-to/platform/cloudflare-tunnel-cluster.nix
instances.tunnel-system = {
cloudflared = {
package = packages.cloudflared;
config = {
image = {
repository = "cloudflare/cloudflared";
tag = "2026.9.0";
};
replicas = 2;
};
};
# The tunnel token, decrypted from the SOPS file at deploy. The
# alias is the dependency name cloudflared reads it under.
tunnel-token = {
package = packages.secret;
aliases = [ "tunnelToken" ];
config = {
keys = [ "token" ];
source = ./cloudflare-token.enc.yaml;
};
};
};

View source on GitHub ↗

cloudflared reads the token from a tunnelToken dependency: a Secret with the key token. The example supplies it as a packages.secret instance whose source is a SOPS file with a top-level token key, decrypted at deploy; the alias tunnelToken connects the two. A packages.secret-ref instance under the same alias names a Secret a secrets operator writes instead. 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:

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": "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:

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:

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 a SOPS-backed packages.secret token, replace the value in the encrypted file and deploy again; Kix updates the checksum/tunnel-token pod annotation from the Secret’s identity hash, which rolls the connector pods automatically. When the token comes from an externally managed packages.secret-ref Secret instead, wire a reloader dependency or restart the connector pods after the external Secret changes.