Tailscale builds a WireGuard mesh with little configuration. Headscale is an open control server for the same clients — you run policy and issue keys yourself. The lab does not use the Tailscale Kubernetes operator; a handful of Deployments and DaemonSets do the job instead.
The goal is one tailnet for laptops and nodes, with Kubernetes APIs and internal HTTP on
*.tif.internal without putting those names on the public internet.
Self-hosting the control plane means you own ACL files, preauth keys, and MagicDNS base domains. The trade-off is operational work: upgrades, backups, and policy edits are yours. For a multi-cluster lab that already runs GitOps everywhere else, that trade-off is acceptable.
Read first
| Piece | Docs |
|---|---|
| Headscale | headscale.net |
| Talos Tailscale extension | Talos network guide |
| Gateway API | gateway-api.sigs.k8s.io |
| Envoy Gateway | gateway.envoyproxy.io |
Public control plane URL: ts.this-is-fine.io. MagicDNS suffix: tif.internal.
What joins the mesh
Headscale (ts.this-is-fine.io)
Talos nodes (Tailscale extension)
kube-apiserver-proxy per cluster (TCP 6443 on tailnet)
tailnet-dns (pods resolve *.tif.internal)
address space: 100.64.0.0/10
kube-apiserver-proxy runs tailscaled and socat. Tailnet clients use hydra-k8s.tif.internal:6443 (pattern:
<cluster>-k8s.tif.internal) to reach the cluster API VIP. tailnet-dns forwards *.tif.internal to Headscale
MagicDNS from inside the cluster.
Pods do not run a Tailscale sidecar in this design. Workloads that need the Kubernetes API or an internal service
use cluster DNS first. The tailnet-dns DaemonSet patches the chain so *.tif.internal resolves the same
way your laptop would on the mesh. Egress from the pod still leaves via the node’s tailnet identity, so ACLs must
allow that path.
HTTP on the tailnet
Attach an HTTPRoute to shared-gateway-tailnet.
Example hostname: vault.tif.internal -> Envoy -> Service. TLS comes from in-cluster PKI via
cert-manager.
tailnet client (HTTPS) -> vault.tif.internal
-> shared-gateway-tailnet
-> Service -> Pods
Public sites use a different gateway (this-is-fine.io, Let’s Encrypt). Keep listeners separate so you do
not accidentally expose an internal-only app.
Policy (sketch)
Headscale ACLs use tags (k8s-api, fabric, per-cluster node tags) and
auto-approved routes for the LoadBalancer slice that fronts the tailnet gateway. Join keys are SOPS-encrypted
bootstrap secrets.
Operator UI: ts.this-is-fine.internal on port 8081 through the internal gateway.
Part of the wider GitOps lab. IRC TLS on the public gateway is covered in the IRC post.