Reaching Kaisin through a Cloudflare Tunnel

Kaisin's default route is Traefik: a DNS record points at the cluster's ingress address and traffic arrives directly. That assumes the cluster has an address the outside world can reach.

Many self-hosted clusters do not. On a home connection, behind CGNAT, or on a private address, the direct route fails in a way that looks like a Kaisin problem and is not:

A tunnel removes the question. A connector inside the cluster dials out to Cloudflare and requests come back down that connection, so nothing needs an inbound route, a forwarded port, or a public address. The hostname resolves to Cloudflare's anycast addresses, which every resolver is happy to return.

Kaisin can do this for you

You very likely do not need the rest of this page.

Give Kaisin a Cloudflare API token on Settings → Domains and DNS and press Apply. It reads the address this cluster answers on, and if that address is one the internet cannot reach it says so and offers a single button: Give this cluster a way in. Pressing it creates a tunnel in your account, runs the connector here, and routes every hostname Kaisin serves through it — no second helm upgrade, no token copied by hand, no terminal.

The token needs Account → Cloudflare Tunnel → Edit, which the setup screen already asks for. What Kaisin creates is named after this installation, so it is recognisable in your dashboard beside tunnels from everything else you run.

Two things it deliberately will not do:

The connector Kaisin starts is desired state in its database, not a value carried by the Helm release. A later helm upgrade leaves it alone, in both directions: it will not remove the one Kaisin runs, and Kaisin will not remove the one the chart installs.

The rest of this page is the manual path. It is still the right one for bootstrap — a cluster nothing can reach cannot show you the panel that would fix it — and for a tunnel you would rather run yourself.

Before you turn it on

A tunnel puts the panel on the public internet. Anyone who knows the hostname reaches the sign-in page. Two things are worth doing at the same time:

Two addresses Access must not guard

A gateway in front of the panel also stands in front of GitHub, and GitHub cannot log in to it. Both of these are answered with a login page instead of reaching Kaisin:

Path What breaks
/api/webhooks/ Nothing deploys on push, and neither pull requests nor matching branches get an ephemeral environment. Kaisin is never told anything happened.
/api/connections/github/ Creating a GitHub App loses it. GitHub hands over the private key exactly once, on the way to this callback — intercept it and the App is left on GitHub with a key nobody holds.

The second is not recoverable. There is no second chance at that key: the only way out is to delete the App on GitHub and create another.

Neither path wants the gateway's protection, because both already defend themselves. A webhook carries GitHub's signature over its body and is rejected without it. The callback spends a single-use handshake that Kaisin minted minutes earlier and refuses a second use.

Kaisin can open them for you. Give its Cloudflare token Account → Access: Apps and Policies → Edit and press Apply on Settings → Domains and DNS; it finds the application in front of the panel and gives those two paths applications of their own that bypass it. The permission is account-wide — Cloudflare scopes Access to an account, not a zone — so it is optional, and without it Kaisin says what it could not check rather than reporting silence as safety.

Or do it by hand. Access has no path exemption inside an application; you add a second application scoped to the longer path, because Cloudflare matches the most specific one first and it inherits nothing from its parent. For each path above: Zero Trust → Access → Applications → Add an application → Self-hosted, with the domain set to panel.example.com/api/webhooks/, and one policy — action Bypass, include Everyone.

Cloudflare also terminates TLS at its edge, so it can see panel traffic in the clear. That is the trade for not opening a port. If that is unacceptable, use a private network — a tailnet or WireGuard — and keep the panel unpublished.

Setup

1. Create the tunnel

Zero Trust → Networks → Tunnels → Create a tunnel → Cloudflared. Name it (kaisin is fine) and copy the token from the install command it shows. The token is a credential: treat it like a password.

2. Add the public hostname

On the tunnel's Public Hostname tab:

Field Value
Subdomain admin
Domain your domain
Type HTTP
URL traefik.kube-system.svc.cluster.local:80

Cloudflare creates the DNS record itself — you do not add one by hand, and you should remove any existing A record pointing at the cluster's private address.

One hostname is enough. Splitting the panel from its API is done inside the cluster by the route Kaisin already installs, so Cloudflare only has to deliver traffic to the ingress.

The URL above is the Traefik service that ships with k3s. On a cluster where the ingress controller lives elsewhere, use that service's address instead.

3. Give the token to Kaisin

Through CI, which is the normal path:

gh secret set CLOUDFLARE_TUNNEL_TOKEN --repo <owner>/<repo>

The deploy workflow enables the tunnel whenever that secret is present, so there is no second switch to forget. Paste the token when prompted rather than putting it on the command line, where it would land in shell history.

Installing directly with Helm instead:

kubectl -n kaisin create secret generic kaisin-tunnel --from-literal=token=<token>

helm upgrade --install kaisin oci://ghcr.io/bluepawlabs/charts/kaisin \
  --namespace kaisin \
  --set cloudflareTunnel.enabled=true \
  --set cloudflareTunnel.existingSecret=kaisin-tunnel

4. Check it

kubectl -n kaisin rollout status deploy/kaisin-tunnel
kubectl -n kaisin logs -l app.kubernetes.io/component=tunnel --tail=20

The label matches either connector — the one this chart installs (kaisin-tunnel) or the one Kaisin starts for itself (kaisin-tunnel-connector). Exactly one of them should be running; if you see both, the second was started before the first and the next reconcile pass removes Kaisin's.

Registered tunnel connection — four of them, one per Cloudflare edge location — means the connector is up. The tunnel's page in the dashboard turns Healthy at the same moment.

Keep TLS on

ingress.tls stays enabled with a tunnel. The certificate is still what lets you reach the panel directly over the local network, and the ClusterIssuer is what certificates for your deployed applications are issued from. Turning it off to "simplify" removes both.

With DNS-01 (tls.solver: dns01-cloudflare) certificates keep renewing whether or not the cluster is reachable, which is the combination that makes a private cluster behave like a normal one.

What changes about routing

Without a tunnel, http:// to the panel redirects to https://.

With a tunnel it is served directly instead: Cloudflare has already terminated TLS at the edge, and the connector reaches Traefik over the cluster network. Redirecting at that point would send the visitor back out to the edge and into a loop. The chart makes this switch on its own.

If the hostname still does not resolve

Check in this order:

  1. kubectl -n kaisin logs -l app.kubernetes.io/component=tunnel — no registered connections means a bad or revoked token.
  2. The tunnel's status in the dashboard. Down is the connector; Healthy with a 502 is the origin URL in the public hostname entry.
  3. Whether an old A record for the hostname is still in DNS. It will shadow the tunnel's CNAME, and it is the record that was failing before.

Browsers cache DNS failures separately from the operating system. After a change, clear chrome://net-internals/#dns or restart the browser before concluding it has not worked.