Host peer relays in Kubernetes with PeerRelay

Last validated:

A PeerRelay is a Custom Resource Definition (CRD) provided by the Tailscale Kubernetes Operator. It lets you deploy one or more peer relay devices inside your cluster that other tailnet devices can use to relay traffic when direct connections aren't possible.

Because a PeerRelay is fronted by a Kubernetes type: LoadBalancer Service, the underlying peer relay pods are reachable at a stable public UDP endpoint even when they restart or reschedule.

Prerequisites

Complete the following before deploying a PeerRelay:

Example PeerRelay configuration

Apply the following manifest to create a PeerRelay with a single replica:

apiVersion: tailscale.com/v1alpha1
kind: PeerRelay
metadata:
  name: my-relay
spec:
  replicas: 1

Configure a grant policy

Before other tailnet devices can use the peer relay, you must create a grant policy that gives them permission to use it. The policy uses the tailscale.com/cap/relay application capability.

By default, the operator tags peer relay devices with tag:k8s. If you use a custom tag, update spec.tags and make sure the operator's tag is an owner of the tag you choose.

Add a grant that lets the intended source devices use the tag your peer relay uses:

{
	"grants": [
		{
			"src": ["tag:private-workloads"],
			"dst": ["tag:k8s"],
			"app": {
				"tailscale.com/cap/relay": []
			}
		}
	]
}

Deploy a PeerRelay

  1. (Optional) Set the tag of the peer relay devices so they are automatically approved. If you set a custom tag, make sure the operator is an owner of that tag.

  2. Create a PeerRelay resource:

    apiVersion: tailscale.com/v1alpha1
    kind: PeerRelay
    metadata:
      name: my-relay
    spec:
      replicas: 1
    

    If you're running on AWS/EKS, this manifest alone is not enough. You must also configure Elastic IPs so the AWS Load Balancer Controller provisions each replica behind a stable public IP.

  3. Wait for the PeerRelay to become ready:

    kubectl wait --for=condition=PeerRelayReady=true peerrelay my-relay
    
  4. Inspect the PeerRelay to verify the public endpoints each replica advertises:

    kubectl get peerrelay my-relay
    
    NAME       AGE   STATUS            ENDPOINTS
    my-relay   2m    PeerRelayReady    203.0.113.10
    

Once at least one replica is ready, tailnet devices with the relay grant automatically discover the peer relay and use it for traffic they can't send directly.

High availability for a peer relay

To improve resilience, run more than one replica of a PeerRelay. Each replica joins the tailnet as its own device with its own public UDP endpoint. If one replica becomes unreachable, tailnet devices with the relay grant fall back to the remaining replicas.

Apply the following manifest to run a PeerRelay with three replicas:

apiVersion: tailscale.com/v1alpha1
kind: PeerRelay
metadata:
  name: my-relay
spec:
  replicas: 3

Each replica of a PeerRelay is fronted by its own LoadBalancer Service, which provisions a separate cloud load balancer (and, on AWS, a separate Elastic IP). Scaling replicas up increases your cloud provider bill accordingly, so pick the replica count that matches your resilience needs rather than the maximum.

On GCP and Azure, spec.replicas is all you need as the cloud controller assigns each Service a distinct public IP. On AWS, you must also configure Elastic IPs.

Customize the LoadBalancer Service

The operator applies default annotations to every LoadBalancer Service it creates so the Service is provisioned with a public IP address on GCP, AWS, and Azure. If you're targeting a different cloud provider or an in-cluster load balancer controller, use spec.service.annotations to supply the annotations your controller expects. These annotations apply uniformly to every replica.

For example, to steer MetalLB to a specific address pool:

apiVersion: tailscale.com/v1alpha1
kind: PeerRelay
metadata:
  name: my-relay
spec:
  replicas: 1
  service:
    annotations:
      metallb.io/address-pool: peer-relays

Deploy on AWS with Elastic IPs

The AWS Load Balancer Controller provisions a Network Load Balancer for each LoadBalancer Service the operator creates. Because AWS NLBs are exposed as DNS names rather than IPs, and because the underlying addresses can shift, spec.aws.elasticIPs lets you pin each replica to a specific Elastic IP (EIP) so the addresses stay stable and each replica is guaranteed a distinct public IP.

Each entry in elasticIPs pairs an EIP allocation ID with a subnet ID in the same availability zone. The operator stamps these values onto the per-replica LoadBalancer Service so the AWS Load Balancer Controller provisions an NLB in the correct AZ and binds the requested EIP.

  1. Allocate one EIP per intended replica in an AZ that has a matching public subnet in your cluster's VPC.

  2. Create the PeerRelay resource with a paired list of allocations and subnets. The list must be at least as long as spec.replicas:

    apiVersion: tailscale.com/v1alpha1
    kind: PeerRelay
    metadata:
      name: my-relay
    spec:
      replicas: 3
      aws:
        elasticIPs:
          - allocationID: eipalloc-0aaaaaaaaaaaaaaaa
            subnetID: subnet-0aaaaaaaaaaaaaaaa
          - allocationID: eipalloc-0bbbbbbbbbbbbbbbb
            subnetID: subnet-0bbbbbbbbbbbbbbbb
          - allocationID: eipalloc-0cccccccccccccccc
            subnetID: subnet-0cccccccccccccccc
    

spec.aws.elasticIPs overrides any service.beta.kubernetes.io/aws-load-balancer-eip-allocations or service.beta.kubernetes.io/aws-load-balancer-subnets annotations supplied via spec.service.annotations. If both are set the per-replica values in spec.aws.elasticIPs take precedence.

Each EIP allocation is scoped to a single availability zone. Make sure the paired subnetID is in the same AZ as the EIP, otherwise the AWS Load Balancer Controller cannot provision the NLB.

For real HA, spread the EIPs across distinct AZs so an AZ outage only affects the replicas in that AZ.

Customization

You can customize the resources the operator creates for a PeerRelay with a ProxyClass. Reference the ProxyClass by name using spec.proxyClass:

apiVersion: tailscale.com/v1alpha1
kind: PeerRelay
metadata:
  name: my-relay
spec:
  replicas: 3
  proxyClass: my-proxy-class

You can find the full list of configuration options in the PeerRelay API reference.

Static endpoints

Peer relays advertise one or more static endpoints so that other tailnet devices can reach them at a stable ip:port even when the device is behind a load balancer or NAT. Outside Kubernetes you configure static endpoints yourself with tailscale set --relay-server-static-endpoints.

The PeerRelay CRD handles this for you: for each replica, the operator reads the public address the cloud has assigned to the replica's LoadBalancer Service and writes it into the replica's tailscaled configuration as the static endpoint. You don't need to run tailscale set on the pods and you don't need to know the public IPs in advance.

Use the CLI approach for peer relays running on standalone devices where you manage the network configuration yourself. Use the PeerRelay CRD for peer relays running inside a Kubernetes cluster.

Verify the peer relay

To verify that peer relay traffic is flowing, generate traffic between two tailnet devices that can't reach each other directly and run tailscale status. When a device uses a peer relay, its connection type is reported as peer-relay:

tailscale status | grep peer-relay

You can also list the peer relays the local device knows about:

tailscale debug peer-relay-servers

Refer to the peer relay documentation for more detail on how peer relay connections are established and verified.

Troubleshooting

If you encounter issues, refer to Troubleshooting the Tailscale Kubernetes Operator.