Tailscale access
Reach the cluster and internal services over the tailnet — no SSH tunnel or EPFL VPN
How it works
The Tailscale Kubernetes operator runs in the cluster (tailscale namespace, installed by the tailscale-operator ArgoCD application) and joins our tailnet as a device named k8s. It does two things:
- Proxies the Kubernetes API on the tailnet in auth mode: your Tailscale identity is impersonated into a Kubernetes group, then normal RBAC applies.
- Exposes selected services as their own tailnet devices (via
ingressClassName: tailscaleor thetailscale.com/exposeannotation), each with a MagicDNS hostname and an HTTPS certificate.
Access is governed by the tailnet ACL policy: group:lcmd members reach exposed services and get read-only kubectl (lcmd:viewers → view); group:lcmd-admins get full kubectl (lcmd:cluster-admins → cluster-admin). The group → cluster-role mapping lives in infrastructure/kubernetes/tailscale/rbac.yaml.
Joining the tailnet
Get invited
Ask an admin to add your account to group:lcmd (and group:lcmd-admins if you need write access to the cluster) in the tailnet ACL policy.
That's it — no VPN, no SSH keys.
Adding a team member (admins)
- Get a Google account email from them — EPFL addresses don't work on our tailnet.
- Users page → Invite external users → send the invite.
- In the ACL policy, add the email to
group:lcmd(every member: exposed services + read-only kubectl). Add togroup:lcmd-adminstoo only if they need full cluster access. - Done — no tags involved. Tags (
tag:k8s,tag:k8s-operator) belong to operator-created devices, never to people. - Verify with them: Tailscale client logged in, then
tailscale configure kubeconfig k8s && kubectl auth whoamishows their email andlcmd:viewers(orlcmd:cluster-admins).
Offboarding is the reverse: remove the email from the groups block, then delete the user from the Users page (their devices drop off the tailnet immediately).
The free plan caps the tailnet at 6 users. Check the Users page count before inviting.
kubectl over Tailscale
With Tailscale connected, one command writes a kubeconfig context pointing at the API-server proxy:
tailscale configure kubeconfig k8s
kubectl get pods -n prodYour access level (read-only vs admin) is determined by your group in the tailnet ACL — there is no token to manage, and audit logs attribute requests to your Tailscale identity.
kubectl port-forward works through this context, so anything previously reachable only via the SSH tunnel (e.g. the ArgoCD UI) works over Tailscale too.
The SSH-tunnel setup described in Kubernetes access still works and remains the break-glass path if Tailscale is down.
Exposed services
| Service | URL |
|---|---|
| Django backend (admin UI) | https://backend.<tailnet>.ts.net/admin/ |
| ArgoCD UI | https://argocd.<tailnet>.ts.net |
Services are published to the tailnet with a Tailscale Ingress (one short hostname per service):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backend-ts
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: backend
port:
number: 8000
tls:
- hosts: [backend] # → https://backend.<tailnet>.ts.netWhen exposing a new hostname that Django serves, also extend ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS in infrastructure/kubernetes/app/overlays/prod/patches/backend.yaml, or logins will fail with CSRF 403s.
The proxy forwards the whole hostname to one service (no path routing). New devices created this way are tagged tag:k8s and are reachable by group:lcmd per the ACL.
Devcontainers
No extra setup: local container runtimes (OrbStack, Docker Desktop) route outbound container traffic through the host's network stack, so the host's Tailscale connection — including MagicDNS and your identity — is shared with the devcontainer. The devcontainer's post-start.sh wires this automatically: it writes a tailscale-k8s kubeconfig context (placeholder token; the proxy authenticates you by tailnet identity) and makes it the default whenever the API proxy is reachable. Verify with kubectl auth whoami — it should print your Tailscale account and groups. If the ts.net hostname doesn't resolve or times out from inside the container (some Linux Docker setups), run your kubectl commands on the host instead; don't build a host-to-container tunnel. Containers on remote hosts (Codespaces, CI) have no host Tailscale to share — that case needs an in-container userspace tailscaled with a tagged ephemeral auth key; ask an admin.
Operator configuration
The install lives in GitOps:
infrastructure/kubernetes/argocd/gitops/tailscale-operator.yaml— the operator Helm chart (pinned version, API-server proxy enabled).infrastructure/kubernetes/tailscale/— namespace, RBAC bindings, and theoperator-oauthsecret (SOPS-encrypted, edit withsops edit).
The OAuth client (Tailscale admin console, scopes Devices Core + Auth Keys, tag tag:k8s-operator) is the only credential; rotating it means generating a new client and re-encrypting operator-oauth.enc.yaml.