Tailscale Kubernetes Operator

May 2, 2026 min read

Setting up Taislcale K8S Operator

As clear as the docs are, I ran into significant errors running this on my homelab. The conbimation of fluxCD, tailscale ACL hyper-specificity, and SOPS secrets made for quite a long process.

Why do I need it?

In my case, I wanted to have remote access to admin data without expoisng those servives directly to the internet. Thankfully, not I have access the the longhorn GUI and Grafana with tailscale, but these are not exposed anywhere on the public web

How did I do it?

Overall, it was very simple - but there were a few tricks that made it much quicker when using fluxCD to manage the config.

First and foremost, you have to add the Owners to your tailscale ACL file:

"tagOwners": {
  "tag:k8s-operator": [your-email@provider.com],
  "tag:k8s": ["tag:k8s-operator"],
}

Both k8s-operator and k8s are required:

From my understanding, k8s-operator is used by by the bootstrap pod to connect your local machines to the tailscale netowrk and the k8s tag is used by the services you create with ingress/egress routes

apiVersion: v1
kind: Namespace
metadata:
  name: tailscale

The namespace has to come first, after that:

apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
  name: tailscale
  namespace: tailscale
spec:
  interval: 1h
  url: https://pkgs.tailscale.com/helmcharts
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: tailscale-operator
  namespace: tailscale
spec:
  interval: 30m
  chart:
    spec:
      chart: tailscale-operator
      version: '1.96.5'
      interval: 30m
      sourceRef:
        kind: HelmRepository
        name: tailscale
        namespace: tailscale
  values:
    operatorConfig:
      hostname: "homelab-tailscale-operator"
    apiServerProxyConfig:
      mode: "true"

Add the helm chart and release values.

However, the biggest piece to the puzzle are the secrets:

#This file MUST have name: operator-oauth
apiVersion: v1
kind: Secret
metadata:
    name: operator-oauth
    namespace: tailscale
type: Opaque
stringData:
    client_id: 0123456789abcdefghijklmnop
    client_secret: 0123456789abcdefghijklmnop

You HAVE to encrypt with SOPS or another manager before committing to git

And finally:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: tailscale-namespace
  namespace: flux-system
spec:
  interval: 1h
  path: ./path/to/namespace.yaml
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: operator-oauth
  namespace: flux-system
spec:
  interval: 1h
  dependsOn:
    - name: tailscale-namespace
  path: ./path/to/secrets
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  decryption:
    provider: sops
    secretRef:
      name: sops-gpg
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: tailscale-config
  namespace: flux-system
spec:
  interval: 1h
  dependsOn:
    - name: operator-oauth # THIS IS THE KEY
  path: ./path/to/configs
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system

Why does this help?

The tailscale kubernetes operator automitically uses a secret named ‘operator-oauth’ with the stringData matching ‘client_id’ and ‘client_secret’
meaning
as long as the secret is created before the config file is applied, the secret automatically gets pulled into tailscale.