Model and how it works
Model
Tailnet(cluster-scoped) — one per Headscale control server: login URL, in-cluster API endpoint, a read-only bootstrap API key from which the operator mints and rotates its own working key. Optionally owns the DNS hosts file and the policy for everything below.TailnetEgress(operator namespace) — one egress identity: a tag, a reusable ephemeral pre-auth key, a userspace agent pod and one Service per target. Consumers inconsumerNamespacesdial<target>.<operator-ns>.svc:<port>; aliases let them dial the real hostname when the far end routes by SNI/Host.TailnetIngress(tenant namespace) — the mirror: expose in-cluster Services on the tailnet as one node with a MagicDNS name and, with a template, a DNS name under your zone. L4 exposes forward TCP; one or moremode: httpexposes (virtual hosts, each with its own name and optional short alias) terminate TLS on the agent with a Secret or cert-manager certificate and forward HTTP or explicitly verified HTTPS to the app, routed by SNI — no shared gateway needed.TailnetSite(cluster-scoped) — a remote network behind a subnet router: a branch/24, a management range, a single host as/32. Declared once in the policy-owning cluster; the router is identified by tag, so any tagged router, in this or another cluster, qualifies. The operator renders theautoApprovers, replays approvals, blocks overlaps withreservedNetworksor older sites and reportsAdvertised/Approved/Ready. Who may reach the site stays a grant in the base policy.TailnetGateway(cluster-scoped) — the subnet router and split-DNS resolver as a CR: one kernel-mode Tailscale Pod advertising the site LAN, the tailnet LoadBalancer range and every egress target, plus a CoreDNS that serves the hosts file as the authoritative zone. Identity is pinned in a state Secret and immutable receipts; fresh-only — it creates a new node and never adopts, restarts or replaces one. Suspended by default.- The tag is the authorization. Each CR is its own node with its own tag; the policy’s grants for
that tag decide what it may reach (egress) or who may reach it (ingress). Without
Tailnet.spec.policythe operator only reads the policy and reports what is missing; with it, the operator renders your base policy plus the CR-derived entries and writes the result to Headscale.
How it works
Six reconcilers, each turning CRs into desired state applied with server-side apply — no per-CR restart to add or remove a target:
- Tailnet — mints the working API key from the bootstrap key, rotates it at half-life (new key verified before the old one is expired), reports reachability.
- Egress / Ingress — per CR: tag pre-check against the live policy, a reusable ephemeral pre-auth
key (rotated at half-life and on server/tag change; a restarted pod re-registers on its own, a
deleted CR’s node ages out),
one agent Deployment, one Service per target, a NetworkPolicy scoped to
consumerNamespaces, and a status that classifies every target route (NotAdvertised/NotApproved/NoGrant) from the node’s netmap and the router’s advertised/approved sets — the two silent failures of a plain proxy, named. - Gateway — creates the subnet-router StatefulSet, its Services and CoreDNS zone exactly once
(immutable receipts pin the identity), then only follows image/route changes into the template and
the hosts file into the zone; it never restarts, adopts or deletes the Pod without
deletionPolicy: Delete. - DNS — folds egress aliases and ingress names into one CoreDNS
hostsConfigMap per Tailnet. The resolver is the cluster’s, not the operator’s; if the operator dies the last file stays valid. - Policy — parses the base HuJSON generically, appends tagOwners /
/32approvals / grants derived from the CRs (base wins, nothing removed, unknown blocks pass through), publishes the render in a ConfigMap and, inApply, writes it when the live document differs. Headscale validates (including the base’stests) before storing, so a rejected render never becomes active. After a successful write it replays missedautoApproversapprovals (a route advertised before its approver existed) on the routers, soNotApprovedheals withoutheadscale nodes approve-routes. - Data plane — one agent pod per CR is a userspace tsnet node.
AGENT_MODEsets the direction and has no default (the operator sets it on every agent Deployment; an unset value is a startup error): egress takes--accept-routesandDials the off-cluster address over the tailnet; ingress listens on the tailnet andDials an in-cluster backend; anhttpexpose terminates TLS on that listener (SNI and Host bound to the ingress DNS name, certificate reloaded from the projected Secret) and proxies HTTP/1.1 and HTTP/2 to the backend. Same binary,io.Copywith half-close, no SOCKS5, no second container; state is in-memory and ephemeral by design.
A single image carries both binaries; the operator runs /tailnet-operator, each agent pod runs
/tailnet-agent.
Source: docs/explanation/model.md at v0.18.0 — found a mistake? Tell us.