Skip to content
Install the operator

Install the operator

Prerequisites

ComponentRequirementWhy
Kubernetes≥ 1.31CRD admission uses the CEL IP library (isIP)
CNINetworkPolicy supportconsumerNamespaces isolation is a NetworkPolicy
Headscale0.29.x, reachable from the cluster over HTTP(S)REST API for nodes, keys and (optionally) the policy
Subnet routera node tagged e.g. tag:fabric advertising the site routes — the operator-managed TailnetGateway (Run the gateway ), or any node you tag yourselfegress targets are reached through it
CoreDNSthe cluster’s, plus (optionally) a resolver you control for your tailnet zoneDNS aliases and ingress names (DNS with CoreDNS )
cert-manageroptionalissues the certificate for a mode: http ingress (Ingress over HTTPS )
Gatewayoptional: Envoy/Gateway API with a wildcard certificate for *.prod-1.tailnet.example.internalHTTPS through a shared gateway instead (Ingress over HTTPS )

Ingress and egress agents are userspace tsnet pods without NET_ADMIN or /dev/net/tun. The opt-in TailnetGateway instead needs a kernel-mode workload with NET_ADMIN, /dev/net/tun and verified forwarding prerequisites. It is fresh-only, not a migration mechanism; read its limits and prerequisites before activation. Installing the chart alone does not create a Gateway.

Helm

helm install tailnet-operator oci://oci.this-is-fine.io/charts/tailnet-operator \
  --version 0.18.0 --namespace tailnet-system --create-namespace

kubectl -n tailnet-system create secret generic headscale-bootstrap --from-literal=apiKey='<key>'

kubectl apply -f - <<'EOF'
apiVersion: tsunaro.io/v1alpha1
kind: Tailnet
metadata:
  name: corp
spec:
  server: https://ts.example.com
  apiUrl: http://headscale.tailnet-system.svc:8080
  apiSecretRef: { name: headscale-bootstrap, key: apiKey }
  proxy: { mtu: 1200, probeInterval: 5m }
EOF

Use your secret manager instead of --from-literal in production. From a source checkout, replace the OCI URL with ./charts/tailnet-operator and set image.repository/image.tag to your build. Helm 3 and 4 are supported; the default chart needs no Prometheus or Grafana CRDs.

To inspect or vendor the chart, pull it and verify its signature first (see SECURITY.md for the key and the Radicle release record):

helm pull oci://oci.this-is-fine.io/charts/tailnet-operator --version 0.18.0
cosign verify --key deploy/cosign.pub --insecure-ignore-tlog \
  oci.this-is-fine.io/charts/tailnet-operator:0.18.0
kubectl -n tailnet-system rollout status deploy/tailnet-operator
kubectl get tailnet corp
kubectl describe tailnet corp

ApiReachable=True and ApiKeyValid=True mean the working key exists and the API answers. The POLICY column stays empty until Operator-owned policy . The Tailnet reports missing credentials and retries until the Secret appears.

Run one operator release per cluster. Leader election is on by default; replicas > 1 adds a PodDisruptionBudget and all replicas share the Lease tailnet-operator.tsunaro.io in the operator namespace. With leaderElection.enabled: false only one replica is allowed and the Deployment uses Recreate. Chart values are documented in the chart README .

Images and private registries

image.digest wins over image.tag. An empty agentImage inherits the operator image. Image-pull Secrets are referenced by name and must exist in each agent namespace (operator namespace for egress, tenant namespace for ingress): imagePullSecrets for the operator pod, agent.imagePullSecrets as the default for agents, Tailnet.spec.proxy.imagePullSecrets to replace that default per Tailnet ([] clears it). The operator never copies registry credentials.

Agent resources

The chart supplies agent requests of CPU 10m and memory 32Mi, and a memory limit of 128Mi. There is no CPU limit. Both ingress and egress select a whole block in this order: CR.spec.resources > Tailnet.spec.proxy.resources > chart agent.resources. Missing keys are not filled from lower tiers. A CR/Tailnet block with both requests and limits unset falls through; either non-nil map selects the block, even if empty in memory. Kubernetes serialization may omit empty maps, so do not rely on them to disable the fallback in a stored CR.

Helm merges supplied values with its defaults: agent.resources: {} does not disable resources. Set agent.resources: null to disable the chart fallback; this renders AGENT_RESOURCES='{}'. Use null for individual chart keys to remove them. A direct binary run with AGENT_RESOURCES absent, empty, or {} retains the zero default. Non-empty input must be one non-null ResourceRequirements JSON object with known fields and valid, nonnegative quantities; requests cannot exceed matching limits.

Installing these defaults or changing effective resources restarts each affected agent with Recreate. Its ephemeral node re-registers, causing a brief interruption. CRs with higher-tier overrides keep their resources. The generated standalone bundle includes the same defaults; customize its operator environment in an overlay.

Network isolation

networkPolicy.enabled isolates the operator pod; networkPolicy.provider picks the dialect.

kubernetes (default) renders a NetworkPolicy for ingress and egress. Set the Kubernetes API and Headscale CIDRs and ports for your site first — NetworkPolicy cannot select a hostname — and list scrapers in networkPolicy.metricsPeers, otherwise metrics ingress is denied.

cilium renders an egress-only CiliumNetworkPolicy instead: ipBlock rules do not match cluster/node identities on Cilium, so the API server is toEntities: kube-apiserver, DNS and Headscale are label selectors (dnsPeers, cilium.headscalePeers; a peer without namespaceSelector is scoped to the operator namespace) and no CIDRs are needed. Ingress stays open (metrics :8080, health :8081). The cilium.io/v2 CRDs must exist; ipBlock peers are rejected at render time.

Both providers allow the same four destinations — API server, cluster DNS, Headscale, agent status on TCP 9080 in every namespace. Agent pods get their own generated policies regardless of provider.

Kustomize

kubectl apply -k deploy

deploy/operator.yaml is generated from the default chart values (task deploy-gen) and contains Namespace, RBAC, ServiceAccount, Deployment and metrics Service; the CRDs are in deploy/crds/. Use an overlay for namespace, images and site policies.

What the chart ships and what the site provides

ComponentChart defaultTurn offSite provides
CRDs (crds/)installed with helm install, never upgraded by Helm--skip-crdsupgrades per Upgrade (task crd-upgrade or Flux crds: CreateReplace)
Deployment, RBAC, ServiceAccount, metrics Service, PDBon; PDB only with replicas > 1— (that is the operator)one release per cluster, the namespace
ServiceMonitor, PrometheusRule, dashboardoffmonitoring.serviceMonitor/rules/dashboard.enabledPrometheus and Grafana Operator CRDs, a Prometheus that selects them
PodMonitor for agents and gatewaysoffmonitoring.podMonitor.enabledPodMonitor CRD; scrapeFrom peers, otherwise the agent policy blocks the scrape
Operator NetworkPolicyoffnetworkPolicy.enabledprovider: kubernetes needs kubeAPICIDRs, headscaleCIDRs, metricsPeers; provider: cilium needs the Cilium CRDs and label peers. For a Headscale outside the cluster (shared/spoke clusters) set cilium.headscalePeers: [] and populate headscaleCIDRs — the chart adds a toCIDR egress on headscalePorts.
Agent NetworkPoliciesalways generated by the operator (not the chart)—consumerNamespaces on the CR; a CNI that enforces NetworkPolicy
Headscalenot shipped—0.29.x reachable from the cluster, bootstrap API key Secret, policy base per Prepare Headscale
PKI for mode: http ingressnot shipped—cert-manager and an issuer, or a TLS Secret; the ingress name must resolve to the agent
Image-pull Secretsnone referenced—Secrets by name in operator and tenant namespaces (imagePullSecrets, agent.imagePullSecrets)
LoadBalancer IPs for gatewaysnot shipped—an LB implementation and pool matching tailnetLBCIDR / service.poolLabels (Run the gateway )
Cluster CoreDNS forwardnot touched—the forward stanza for the alias zones (Wire the cluster CoreDNS )
Flux/cosign verificationpublic key in deploy/cosign.pub—the verify block on OCIRepository/HelmRelease (SECURITY.md )
Tailnet, TailnetEgress, TailnetIngress, TailnetSite, TailnetGatewaynone created—every CR; the chart installs only the schemas

Source: docs/how-to/install-operator.md at v0.18.0 — found a mistake? Tell us.