Skip to content
NebulaCtrldocs
Guides

Expose a service only through Tailscale

Connect a cluster to your tailnet, then give a service a tailnet-only domain under your ts.net name so only devices signed into your tailnet can reach it.

A tailnet domain has no public route. Tailscale supplies its hostname, a name under your tailnet's ts.net domain, and its certificate. This guide connects a cluster to your tailnet, adds the domain, and removes every public route to the service.

Before you begin

  • You have the admin role to connect the cluster, and the deployer role or higher to add the domain. See Members and teams.
  • The cluster is connected. See Connect a cluster.
  • You can run a command with sudo on the cluster's server node. The command does not run on a worker. The node runs Ubuntu 22.04 or newer, or Debian 12 or newer.
  • You have a Tailscale OAuth client. Per the API's definition it has write scope for Devices (Core), Keys (Auth Keys) and General (Services), under the operator's own tag. Keep its client ID and client secret at hand.
  • The service has an HTTP or reverse-proxy process. See Processes.

Steps

Store the OAuth client

Select Clusters, select the cluster, open its Settings tab, and find Tailnet. The Tailscale row reads Not connected.

In the OAuth client row, fill Client ID and Client secret, then select Connect. A toast reads Tailscale connected and the row shows an Install command and a One-time token. The secret is never shown again. Connect stays disabled until both fields are filled, and connecting again replaces the stored client.

If the toast reads Tailscale was not connected, it names the cause. If the cluster is not connected, the cause is that it cannot issue a command yet.

Run the command on the server node

Copy the Install command, run it in a shell on the cluster's server node, and paste the One-time token at the NebulaCtrl token: prompt. The prompt does not echo. The token is single-use and the row says when it expires.

The script installs Helm if it is missing, installs Tailscale Kubernetes operator 1.102.4 into the tailscale namespace, gives the agent read access to that namespace, and creates the nebulactrl-egress egress ProxyGroup. It waits up to 180 seconds for the operator and up to 180 seconds for the ProxyGroup. It is safe to run again, and it writes /var/log/nebula-tailscale.log. It ends with Done. This cluster is connected to your tailnet.

If it stops early, it prints why. Two examples: this host is not enrolled in this cluster; run install.sh here first and this host has no local admin kubeconfig; run tailscale.sh on this cluster's own server node, not a worker.

Confirm the cluster is connected

Reopen Settings. The Tailscale row reads Connected once the agent reports the operator. Until then, Tailnet is unavailable when you add a domain.

Add the tailnet domain

Select the service on the canvas, open its Settings tab, scroll to Networking and select Add domain. Enter the Hostname and the Process, then choose Tailnet under Exposure. The dialog replaces the DNS and certificate choices with a note. Select Add domain.

The first label of the hostname becomes the device name: hausbai or hausbai.example.com both ask for the device hausbai. The hostname follows the same rules as for a custom domain. A toast reads HOSTNAME — reachable from your tailnet once Tailscale picks it up.

If Tailnet is disabled, its card says why: This environment has no cluster bound, so there is nothing to ask., This cluster has never connected, so its capabilities aren't known yet. or This cluster hasn't connected to a tailnet yet. Connect it from its settings. The dialog links Open cluster settings.

Remove every public route

The service is exposed only through Tailscale when none of its domains is public. Every domain in Networking without a Tailnet badge is public. For each one, open Actions for HOSTNAME, then either select Remove domain, or select Domain settings…, choose Tailnet and select Save. Switching to tailnet turns off Cloudflare DNS and DNS-01 certificates for that domain.

A service with a port keeps its cluster-only address, shown as Internal · cluster-only.

To store the OAuth client and add the domain over the API:

curl -X POST "BASE_URL/api/v1/clusters/CLUSTER_ID/tailscale" \
  -H "Authorization: Bearer NEBULA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"clientId":"TS_CLIENT_ID","clientSecret":"TS_CLIENT_SECRET"}'

The answer holds command, token and expiresAt. Run command on the server node as in step 2.

curl -X POST "BASE_URL/api/v1/environments/ENVIRONMENT_ID/domains" \
  -H "Authorization: Bearer NEBULA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"hausbai","serviceId":"SERVICE_ID","processId":"PROCESS_ID","exposure":"tailnet"}'

BASE_URL is your control plane address and NEBULA_TOKEN is an API token without project limits, which the cluster call requires. CLUSTER_ID, ENVIRONMENT_ID, SERVICE_ID and PROCESS_ID are resource ids. TS_CLIENT_ID and TS_CLIENT_SECRET are the OAuth client's values.

Verify

The status beside the domain reads Private to tailnet and the hostname becomes a link to https://hausbai.EXAMPLE.ts.net, where EXAMPLE is your tailnet's name. A line under the status reads Reported by the cluster and a time. Open the link from a device signed into your tailnet. A device outside it cannot reach the address.

Fix a domain that is not ready

StatusWhat it meansWhat to do
Not reported yetThe cluster's agent has not reported on the domain.Check that the cluster is online under Clusters.
Setting upThe operator has not yet exposed the domain.Wait. If the service has no healthy release, the card reads HOSTNAME goes live when SERVICE has a healthy release and the reason. Fix the deployment.
Needs attentionThe agent found a problem. The card shows it in full.See below.

Messages under Needs attention:

  • the Tailscale operator is not installed on this cluster (there is no IngressClass named tailscale)… or the Tailscale operator is not running (0 replicas ready in tailscale/operator)…: repeat steps 1 and 2.
  • Tailscale has not reported an address for HOSTNAME after 3m0s; the operator did not say why.: run kubectl -n tailscale logs deploy/operator on the server node. The message names a missing OAuth client or an access control list (ACL) that does not allow the operator's tag as the usual causes.
  • Tailscale named this domain's device "hausbai-1" instead of "hausbai"…: another device on your tailnet has the name. Remove that device in the Tailscale admin console and the domain takes its name.
  • …the cluster reported the tailnet…, but this organization's other tailnet domains are on…: the cluster is on a different tailnet from the organization's other tailnet domains. Connect it to the same tailnet.

Clean up tailnet devices

Each tailnet domain is a device on your tailnet. When you remove the domain, the Tailscale operator removes its device. If the device is still there after 10 minutes, NebulaCtrl removes it with the stored OAuth client. It runs this check every 5 minutes, and only for a device it recorded that carries the operator's tag:k8s.

When a device remains, the cluster's Overview tab shows a warning titled 1 device from a removed tailnet domain is still on your tailnet, or N devices from removed tailnet domains are still on your tailnet. Each device is listed with its name and the reason. Select Open Tailscale machines to remove it by hand.

The reason is its domain is gone; NebulaCtrl removes it if the Tailscale operator has not within 10m0s while the wait runs. After a failure it names the cause: the removal call failed, the stored OAuth client is unusable (reconnect the cluster in steps 1 and 2), or the device no longer carries tag:k8s and is never removed by NebulaCtrl. To list devices over the API, call GET /api/v1/clusters/CLUSTER_ID/tailnet-devices (listClusterTailnetDevices).

Remove a tailnet domain

Removing a domain cannot be undone. Adding it again starts from scratch.

Open Actions for HOSTNAME, select Remove domain, and confirm. A tailnet domain has no Check now action. The device leaves your tailnet as described above.

Next steps

On this page