Quickstart tutorial for the Tailscale Kubernetes Operator
Last validated:
This topic covers various scenarios that demonstrate the Tailscale Kubernetes Operator. You securely expose services, connect multiple clusters, and set up high-availability services, all using Tailscale.
What the Tailscale Kubernetes Operator does
The Tailscale Kubernetes Operator provides secure connectivity for your Kubernetes clusters. Here are some use cases.
- Secure access: Securely access the Kubernetes API server over Tailscale, without exposing it to the public internet.
- Expose services: Expose services deployed in your cluster to your tailnet, and optionally to the public internet through Tailscale Funnel.
- Connect to other services: Enable pods in your cluster to connect to services on your tailnet, including services in other clusters.
- Subnet routers and exit nodes: The Operator can also act as a subnet router or an exit node.
Prerequisites
Before you begin, make sure you have the following:
- A Tailscale account.
kubectlinstalled and configured.helminstalled.kindinstalled. This is optional for local clusters.
Setup
This section covers the initial setup. Start by downloading the getting-started scripts and manifests:
curl -sL https://tailscale.com/k8s/getting-started.tar.gz | tar xz
cd getting-started
This downloads and extracts a getting-started/ directory containing the step scripts and Kubernetes manifests used throughout this topic.
Enable HTTPS for your tailnet
Refer to Enabling HTTPS for instructions.
Configure Tailscale ACLs
The Operator requires specific ACLs to manage devices and services in your tailnet. This is also where you create the tag:k8s-operator that you need in the next section.
-
Go to the Access controls in the admin console.
-
Add the following to your ACLs:
{ "tagOwners": { "tag:k8s-operator": ["autogroup:admin"], "tag:k8s": ["tag:k8s-operator"], }, "autoApprovers": { "services": { "tag:k8s": ["tag:k8s"], }, }, "acls": [ // Allow everything to talk to everything on the tailnet. This should // be used on personal and test tailnets only. {"action": "accept", "src": ["*"], "dst": ["*:*"]}, ], "grants": [ // Allow everything on the tailnet to talk to the API Server Proxy. This should // be used on personal and test tailnets only. { "src": ["*"], "dst": ["tag:k8s-operator"], "ip": ["tcp:443"] } ], "nodeAttrs": [ // Let the Kubernetes operator use Tailscale Funnel { "target": ["tag:k8s"], // tag that the Tailscale operator uses to tag proxies; defaults to 'tag:k8s' "attr": ["funnel"], }, ], }If you already have a
tagOwners,grants,nodeAttrs,autoApproversoraclssection, merge these rules into your existing configuration.
Create OAuth credentials and configure secrets
The Tailscale Kubernetes Operator needs OAuth credentials to interact with the Tailscale API on your behalf. For this topic, you need to create two separate OAuth clients and configure the corresponding Kubernetes secrets.
Cluster 1:
-
Go to the OAuth clients in the admin console.
-
Select + Credential.
-
Give the client a description, for example,
cluster-1-operator. -
Add the following scopes:
- General > Services:
ReadandWrite - Devices > Core:
ReadandWrite - Keys > Auth Keys:
ReadandWrite
- General > Services:
-
In the Tags box, select Add tags and select tag:k8s-operator. If you cannot find this tag in the list, refer to the previous section.
-
Select Generate credential.
-
Copy the Client ID and Client secret.
-
Open
cluster-1-oauth.yamland enter the Client ID and Client secret in the corresponding fields. The credentials should be in plaintext.apiVersion: v1 kind: Secret metadata: name: operator-oauth namespace: tailscale stringData: client_id: "your-client-id-for-cluster-1" client_secret: "your-client-secret-for-cluster-1"
Cluster 2:
Repeat the process above to generate and populate the credentials in the cluster-2-oauth.yaml secret. Be sure to give the OAuth client a unique description, for example cluster-2-operator.
Run the setup script
Now you are ready to set up the Kubernetes clusters and install the Tailscale operator on them. The 00-setup.sh script handles this for you. You have two options for which clusters to use:
Option 1: Use kind to create local clusters
If you have kind installed, you can have the script create two local clusters for you.
Run the script from the root of the repository:
./00-setup.sh
The script:
- Creates two
kindclusters namedcluster-1andcluster-2. - Installs the Tailscale Kubernetes Operator in each cluster using Helm.
- Applies the OAuth secrets you created.
- Configures the Operator for each cluster.
Option 2: Bring your own clusters
If you want to use your own existing Kubernetes clusters, specify their kubectl contexts as environment variables.
If you choose this option, Tailscale recommends consulting the Operator documentation to check Kubernetes version and CNI compatibility.
To enable the first section (Secure API Server Access), the setup script creates a ClusterRoleBinding that grants your tailnet user cluster-admin on the cluster. Do not run this against production clusters.
When you're ready, run the script with the CLUSTER_1 and CLUSTER_2 environment variables set to your cluster contexts:
CLUSTER_1=<your-cluster-1-context> CLUSTER_2=<your-cluster-2-context> ./00-setup.sh
The script then performs the same operator installation and configuration steps on your specified clusters.
After the script finishes, you have two Kubernetes clusters ready to go.
Use cases
Now that the setup is complete, try out some use cases for the Tailscale Kubernetes Operator.
Access the API server securely
Use the Tailscale Kubernetes Operator to securely access your cluster's API server over your tailnet.
When the Operator was installed by the 00-setup.sh script, it was configured with the --set-string apiServerProxyConfig.mode="true" flag. This enables the API server proxy feature, which runs a proxy in the Operator's pod that lets you securely access the Kubernetes API server over the tailnet.
To access the API server over Tailscale, update your kubeconfig to point at the proxy. Use the tailscale configure kubeconfig command, which adds a new context to your kubeconfig that routes through the Tailscale proxy:
tailscale configure kubeconfig <operator-hostname>
To make things faster, the 01-api-access.sh script does this for you. It waits for the API server proxy to be ready, then runs tailscale configure kubeconfig for both clusters.
-
Run the script:
./01-api-access.sh -
After the script completes, you can access your clusters using the new
kubeconfigentries. Try running akubectlcommand against one of the clusters:kubectl get nodes
You are now accessing your cluster's API server securely over Tailscale, without exposing it to the public internet.
What about RBAC?
You might be wondering how Role-Based Access Control (RBAC) works with the API server proxy. The 00-setup.sh script creates a ClusterRoleBinding to grant your Tailscale user cluster-admin privileges:
kubectl --context ${CLUSTER_1} create clusterrolebinding ts-cluster-admin --clusterrole=cluster-admin --user=${ts_user}
The Tailscale API Server Proxy embeds the user's identity into the request headers. This means you can use standard Kubernetes RBAC to control access based on Tailscale identities.
You can inspect the ClusterRoleBinding by running:
kubectl describe clusterrolebinding ts-cluster-admin
The output displays your Tailscale user identity (typically your email address) in the Subjects field.
Name: ts-cluster-admin
Labels: <none>
Annotations: <none>
Role:
Kind: ClusterRole
Name: cluster-admin
Subjects:
Kind Name Namespace
---- ---- ---------
User alice@example.com
For more information, refer to Access the Kubernetes API server over Tailscale.
Expose a service with a Tailscale Ingress
Next, expose a workload running in your cluster to your tailnet by using a Tailscale Ingress. This section uses a Tailscale-themed demo web app.
-
Deploy the "Tailnet Connect" game to
cluster-1:./02-tailnet-connect-game.shThis script applies the
ingress/deploy.yamlmanifest, which creates a standard KubernetesDeploymentandServicefor the application. -
Expose the game to your tailnet using a Tailscale Ingress:
./03-tailnet-connect-game-ingress.shThis script applies the
ingress/ingress.yamlmanifest. The key parts of this file are:# ingress/ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: tailnet-connect-game spec: defaultBackend: service: name: tailnet-connect-game port: number: 80 ingressClassName: tailscale tls: - hosts: - tailnet-connect-gameBy setting
spec.ingressClassNametotailscale, you are asking the Tailscale Kubernetes Operator to handle thisIngress. The Operator provisions a new Tailscale node in your tailnet and proxies traffic to the backend service. -
It may take a minute or two for the
Ingressto be fully configured and for the TLS certificates to be issued and configured. You can check its status by running:kubectl get ingress tailnet-connect-gameAfter the
ADDRESSfield is populated with a MagicDNS name, copy it and open it in your web browser to play the game.
Expose a service with Funnel
Now, expose the same service to the public internet using Tailscale Funnel.
-
Enable Funnel for the
Ingress:./04-tailnet-connect-game-funnel.shThis script applies the
ingress/ingress-funnel.yamlmanifest, which adds atailscale.com/funnel: "true"annotation to theIngresscreated in the previous step. This tells the Operator to expose the service publicly through Funnel.The resulting
Ingresslooks like this:# ingress/ingress-funnel.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: tailnet-connect-game annotations: tailscale.com/funnel: "true" spec: defaultBackend: service: name: tailnet-connect-game port: number: 80 ingressClassName: tailscale tls: - hosts: - tailnet-connect-game -
You can find the public URL in the
ADDRESSfield of theIngressusingkubectl get ingress tailnet-connect-game. -
Access the game from your browser using the public URL. You can also try turning Tailscale off on your device to confirm it's accessible from the public internet.
For more information, refer to Expose cluster workloads to your tailnet with Ingress.
Multi-cluster connectivity
This section covers how to connect services running in different clusters. Deploy the demo application to both clusters and configure them to communicate with each other. The demo application has two modes: "game" mode, which you've seen, and "visualizer" mode. In visualizer mode, it attempts to connect to a peer service. You should observe some interesting behavior from it as it tries to connect to the peer service.
-
Deploy the application to both clusters:
./05-multi-cluster-deploy.shThis script deploys the application to both clusters. Examine
cross-cluster/tailnet-connect-1.yaml:# cross-cluster/tailnet-connect-1.yaml apiVersion: v1 kind: Service metadata: name: tailnet-connect-1 spec: type: LoadBalancer loadBalancerClass: tailscale ... --- apiVersion: apps/v1 kind: Deployment ... env: - name: PEER_URL value: "http://tailnet-connect-2-egress.default.svc.cluster.local"Setting
spec.typetoLoadBalancerandspec.loadBalancerClasstotailscaletells the Operator to expose thisServiceto the tailnet with a MagicDNS name. TheDeploymentis configured to talk to a service namedtailnet-connect-2-egress, which you set up next. A similar configuration is applied forcluster-2. -
Access the web UI. You can access the web UI of the application running in either cluster on the MagicDNS name:
http://default-tailnet-connect-1.<your-tailnet>or
http://default-tailnet-connect-2.<your-tailnet>After a few seconds, the web UI indicates that it was unable to connect to the peer service over the tailnet.
-
Configure cross-cluster communication:
./06-multi-cluster-connect.shThis script enables pods in one cluster to talk to a service in another cluster. It does this in two steps for each direction. First, it creates a Kubernetes
Service:# cross-cluster/tailnet-connect-2-egress.yaml (applied to cluster-1) apiVersion: v1 kind: Service metadata: name: tailnet-connect-2-egress spec: type: ExternalName externalName: placeholder # This will be ignoredThis Kubernetes
Serviceis of typeExternalName, as the Operator needs this to route traffic to the Tailscale-managed proxy.Second, it annotates this service with the MagicDNS name of the service in the other cluster:
kubectl annotate service tailnet-connect-2-egress \ tailscale.com/tailnet-fqdn="default-tailnet-connect-2.${magic_dns}"The
tailscale.com/tailnet-fqdnannotation tells the Tailscale Kubernetes Operator's egress proxy to resolve requests fortailnet-connect-2-egress.default.svc.cluster.localto the specified MagicDNS name on the tailnet. This lets the application incluster-1connect to the application incluster-2using a standard Kubernetes service name. -
Now, if you go back to one of your browser tabs for either cluster (using the MagicDNS names
default-tailnet-connect-1.<your-tailnet>ordefault-tailnet-connect-2.<your-tailnet>), Tailscale logo displays in the UI go from random flashing dots to the dots arranging into a Tailscale logo, and a green "online" indicator below it. The applications are talking to each other across clusters.
For more information, refer to Access tailnet resources from your cluster with Egress.
High availability
You can set up a high-availability service with replicas in both clusters. Tailscale's multi-cluster ingress capabilities let you load balance traffic across services in different clusters.
-
Deploy the HA service. To make it easier to distinguish between the clusters, set the
CLUSTER_1_COLORandCLUSTER_2_COLORenvironment variables to a color of your choice when running the script. For example:CLUSTER_1_COLOR=orange CLUSTER_2_COLOR=purple ./07-tailnet-connect-game-ha.shThis script applies two key manifests to each cluster. First,
multi-cluster-replica/proxy-group.yamldefines aProxyGroup, which is a custom resource that deploys a group of Tailscale proxies in the Tailscale namespace. ThisProxyGrouphas aspec.typeofingress, so it is configured to be used for ingress. Thisspec.typefield can also be set toegressorkube-apiserverdepending on how you plan to use theProxyGroup.You can also use
spec.replicasto deploy replicas for high availability.# multi-cluster-replica/proxy-group.yaml apiVersion: tailscale.com/v1alpha1 kind: ProxyGroup metadata: name: ingress-proxies spec: type: ingressSecond,
multi-cluster-replica/service.yamlcreates aLoadBalancerservice that uses thoseProxyGroupproxies (through thetailscale.com/proxy-groupannotation) to make the backend accessible to the tailnet.# multi-cluster-replica/service.yaml apiVersion: v1 kind: Service metadata: name: tailnet-connect-game annotations: tailscale.com/proxy-group: ingress-proxies # Assigns this service to the ProxyGroup tailscale.com/hostname: tailnet-connect-game-multi-cluster # Sets the MagicDNS name for the HA service spec: type: LoadBalancer loadBalancerClass: tailscale # Specifies that Tailscale should manage this LoadBalancer ...By applying these manifests to both clusters, you are telling the Tailscale Kubernetes Operator in each cluster to configure the
ProxyGroupto advertise the hostnametailnet-connect-game-multi-clusteron theProxyGroupingress-proxies. The KubernetesServicein each cluster is now available at the same MagicDNS nametailnet-connect-game-multi-cluster.<your-tailnet>.The
ProxyGroupfeature uses Tailscale Services. Refer to the documentation for more information. -
Access the application using the MagicDNS name
http://tailnet-connect-game-multi-cluster.<your-tailnet>. The "Tailnet Connect" game will display with the color of the dots indicating which cluster is serving the request. -
The same colored dots might display on every refresh, which might not be what you'd expect from regular round-robin load balancing. This is because your Tailscale client tries to find the optimal route to the backend, and it might prefer one destination over the other.
To simulate a failure and view the failover in action, scale down the deployment in the cluster whose color you are currently seeing.
# For cluster-1 kubectl scale deployment tailnet-connect-game --replicas=0 # For cluster-2 kubectl scale deployment tailnet-connect-game --replicas=0 -
Refresh the page in your browser. After a few seconds, you can observe the game is still running, but now with different colored dots. You are now being served from the other cluster. The
ProxyGroupautomatically detected the failure and unadvertised itself for this MagicDNS name, causing all traffic to be routed to the healthy replica.
For more information, refer to Connect services across clusters.
Further exploration
- Understand the Tailscale Kubernetes Operator to learn about its architecture and the custom resources it manages.
- Manage and configure the Tailscale Kubernetes Operator to operate and tune your deployment.
- Tailscale Kubernetes Operator reference for detailed reference documentation.