Private Access Without a Public Load Balancer

Internal tools, admin APIs, staging backends: most teams run a few services that should never sit behind a public endpoint, yet engineers still need to reach them from their laptops. The classic answers are a bastion host, a VPN appliance, or an IP allowlist on a public load balancer. All three add infrastructure to maintain and tend to open more of the network than anyone intended.

Tailscale handles the connectivity. On Kubernetes, the hard part is deciding what your tailnet can reach once it is connected to the cluster.

Two choices decide whether the setup stays safe. The first is using a subnet router when one service was enough, which puts every IP in the advertised range on the tailnet, pods and databases included. The second is keeping Tailscale's default allow-all policy, which lets every device on the tailnet reach every proxy the operator creates.

Pick the Pattern First

The Tailscale Kubernetes Operator supports three ways to connect a cluster to a tailnet. Choose one before installing anything, then design the access policy around it.

  • Expose one service. An annotation on the service's Kubernetes Service creates a dedicated tailnet device. Only that service is reachable, and only on its declared ports. This is the right default.
  • Subnet router. A Connector advertises a whole CIDR. Every pod, every RDS instance, and every peered VPC in that range becomes reachable. Use it only for people who need network-level access.
  • Egress. The cluster calls a machine that lives only on the tailnet. Nothing comes in.

Three Tailscale exposure patterns side by side: exposing one service, where only that service is reachable from the tailnet; a subnet router, where the whole VPC including RDS is reachable; and egress, where the cluster calls out to a tailnet machine and nothing comes in
Three Tailscale exposure patterns side by side: exposing one service, where only that service is reachable from the tailnet; a subnet router, where the whole VPC including RDS is reachable; and egress, where the cluster calls out to a tailnet machine and nothing comes in

How It Fits Into Qovery

There is no custom integration. The operator runs as a regular Qovery Helm service, and you expose services with features you already use:

  • Helm service: the official tailscale-operator chart runs in its own environment, one per cluster, with a pinned version, so every upgrade is a reviewed change.
  • Secret variables: the Tailscale OAuth credentials are stored as Qovery secrets and injected into the chart values at deploy time, so they never appear in the override file.
  • Annotation groups: exposing a service is a matter of attaching an annotation group to it. Detach the group and redeploy, and the tailnet device disappears.
  • Tailscale grants: a short policy restricts each exposed service to the group that needs it, on the ports it needs.

At the end, one Qovery application is reachable at a stable tailnet hostname, only on its declared ports, and only by the team you choose. The rest of the VPC stays off the tailnet.

Tested End to End

I built this setup on a Qovery-managed EKS cluster, hit the real failure modes (OAuth scopes, tag ownership, the default allow-all policy), and documented the fixes. The full walkthrough covers the tailnet preparation, the Helm service configuration, the annotation groups, per-service tags, the optional subnet router and egress patterns, and a troubleshooting table for the errors you are most likely to see.

Follow the Step-by-Step Tutorial

The full tutorial is in the Qovery documentation:

Expose a Qovery Service on Your Tailnet with Tailscale →