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 public name pointing at
10.x,192.168.xor172.16–31.xis discarded by most resolvers and home routers as DNS rebinding protection. The record is correct and still does not resolve. - Networks that force DNS through their own resolver make this impossible to diagnose with
dig, which talks to port 53 directly and simply times out. - A valid certificate does not help. The certificate proves who you are reaching, not whether you can reach it.
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:
- It never deletes a tunnel it did not create. A tunnel carries the CNAME for every hostname routed through it, so deleting one takes live sites down. Kaisin records which tunnel it made, and Disconnect removes only that one. A tunnel you made yourself is left standing and only the connector stops.
- It never runs a second connector beside yours. If this chart is already running one, Kaisin adopts it rather than starting a rival. Two connectors on one tunnel are HA peers — Cloudflare balances across them — so a second one in a different cluster answers roughly half of every request, from the wrong place.
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:
- Put Cloudflare Access in front of it — Zero Trust → Access → Applications. Requests are then gated on your identity provider before they ever reach Kaisin, which is a far stronger position than a password on an exposed login form. Read the next section before you do: two addresses have to stay open, and one of them fails in a way you cannot undo.
- Change the bootstrap password if you have not already.
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:
kubectl -n kaisin logs -l app.kubernetes.io/component=tunnel— no registered connections means a bad or revoked token.- The tunnel's status in the dashboard. Down is the connector; Healthy with a 502 is the origin URL in the public hostname entry.
- Whether an old
Arecord 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.