Managing access to Kubernetes clusters across multiple environments can quickly become complex. In the past, I had to distribute kubeconfigs and manage client certificates for every engineer. I wanted to make secure access to Kubernetes clusters more straightforward.
Headscale & Tailscale
In my company, every employee has access to the corporate network via Tailscale. We use Headscale as a self-hosted control plane. Within the corporate network, every user is identified by their Tailscale IP.
Idea: Map Tailscale Identity → Kubernetes User
Kubernetes supports a feature called User Impersonation. It allows a trusted component to make API requests on behalf of another user by setting specific HTTP headers.
In particular, the following headers are relevant:
Impersonate-UserImpersonate-Uid
When these headers are set, the Kubernetes API server evaluates the request using the impersonated identity instead of the original authenticated user. Authorization is then enforced via RBAC as usual.
This feature is typically used by API gateways or aggregation layers. In this project, it serves as the bridge between Tailscale identity and Kubernetes RBAC.
Implementation
The solution consists of a small reverse proxy that runs inside the Kubernetes cluster. It exposes the Kubernetes API through a Tailscale-enabled endpoint and performs identity translation.
The proxy has three responsibilities:
- Accept incoming connections from the Tailscale network
- Resolve the Tailscale identity of the caller
- Forward the request to the Kubernetes API using impersonation headers
The proxy itself authenticates to the Kubernetes API using a ServiceAccount token.
Creating a Tailscale-enabled HTTP Server
Instead of running a separate tailscaled process, the proxy uses the tsnet package. This allows embedding a Tailscale node directly into the Go application.
s := &tsnet.Server{
Hostname: "tailscale-kube-proxy",
}
defer s.Close()
lc, err := s.LocalClient()
if err != nil {
log.Fatal(err)
}
Extracting the Tailscale Identity
When a request arrives, we use WhoIs() to determine which Tailscale user is associated with the source IP address.
who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
This call resolves the Tailscale node and provides access to the authenticated user's profile information, including the login name.
Injecting Kubernetes Impersonation Headers
The reverse proxy modifies outgoing requests before forwarding them to the Kubernetes API.
proxy.Director = func(r *http.Request) {
originalDirector(r)
// Clear any existing impersonation headers to prevent header injection
r.Header.Del("Impersonate-User")
r.Header.Del("Impersonate-Group")
// Identify the Tailscale user making the request based on their IP
who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
if err == nil {
// Set Kubernetes impersonation headers to enable RBAC based on Tailscale identity
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
Before setting impersonation headers, existing headers are removed to prevent header injection attacks.
The proxy authenticates itself to Kubernetes using a ServiceAccount token while impersonating the Tailscale user.
Required RBAC Permissions
For impersonation to work, the ServiceAccount used by the proxy must be allowed to impersonate users:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tailscale-kube-proxy-impersonator
rules:
- apiGroups: [""]
resources: ["users", "groups", "serviceaccounts"]
verbs: ["impersonate"]
Usage / Deployment
The tailscale-kube-proxy project is available on GitHub. Once the proxy is built or the image pulled, here’s how to deploy and use it.
1. Create a ServiceAccount and RBAC Permissions
The proxy requires permission to impersonate users in Kubernetes and to manage its own secrets. For this, create a ServiceAccount, ClusterRole, and ClusterRoleBinding:
apiVersion: v1
kind: ServiceAccount
metadata:
name: tailscale-kube-proxy
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tailscale-kube-proxy-impersonator
rules:
- apiGroups: [""]
resources: ["users", "groups", "serviceaccounts"]
verbs: ["impersonate"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tailscale-kube-proxy-impersonator
subjects:
- kind: ServiceAccount
name: tailscale-kube-proxy
namespace: kube-system
roleRef:
kind: ClusterRole
name: tailscale-kube-proxy-impersonator
apiGroup: rbac.authorization.k8s.io
Note: This ClusterRole allows access to all secrets. Currently, this is acceptable because the proxy only manages its own secret, but improvements are possible to further restrict access.
2. Create a Tailscale Auth Key
To join the Tailscale network, the proxy needs a pre-authentication key. With Headscale, you can generate one for a user:
headscale preauthkeys create --user <USER_ID>
Store the auth key in a Kubernetes secret:
apiVersion: v1
kind: Secret
metadata:
name: tailscale-kube-proxy-secret
namespace: kube-system
type: Opaque
stringData:
authKey: "PUT_YOUR_AUTH_KEY_HERE"
This secret allows the proxy to authenticate to the Tailscale network and persist necessary network data.
3. Deploy the Proxy
Finally, deploy the proxy as a single pod in your cluster:
apiVersion: apps/v1
kind: Deployment
metadata:
name: tailscale-kube-proxy
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: tailscale-kube-proxy
template:
metadata:
labels:
app: tailscale-kube-proxy
spec:
serviceAccountName: tailscale-kube-proxy
containers:
- name: proxy
image: ghcr.io/0x2321/tailscale-kube-proxy:latest
env:
- name: SECRET_NAME
value: "tailscale-kube-proxy-secret"
- name: HOSTNAME
value: "awesome-cluster"
4. Accessing the Cluster
To use the proxy, simply connect to the Tailscale network and point kubectl to the proxy endpoint:
export KUBERNETES_SERVER=https://tailscale-proxy
kubectl --server=$KUBERNETES_SERVER get pods
The proxy automatically determines your Tailscale identity and enforces the correct permissions in Kubernetes.
Kubernetes API access over Tailscale
TailscaleKubeProxy lets you securely access Kubernetes using your Tailscale identity — no kubeconfigs or certificates needed.