Changelog
Notable changes are recorded here using Keep a Changelog .
[Unreleased]
[0.18.0] - 2026-10-17
Added
- Chart: the operator Deployment sets
GOMEMLIMITfrom its ownresources.limits.memorythrough the Downward API, so the Go runtime collects before the cgroup limit instead of being OOM-killed at it. Asserted intest/chart.
Changed
- Chart: default operator
resources.limits.memory128Mi → 192Mi (requests unchanged). Operators with a few dozen CRs settled a few MiB below the old limit. The agent default block is unchanged; its comment states that tenant LimitRanges must admit these values. - Docs, examples and test fixtures use RFC 2606/5737 names and addresses throughout
(
corp.example,partner.example,prod-1.tailnet.example.internal,198.51.100.0/24, …). The gateway reference drops the “Verified platforms” table;compatibility.mdlistslinux/amd64andlinux/arm64, and the preflight note states that the image is multi-arch. - Docs describe the
TailnetGatewayas the operator-managed router and resolver; “bring your own resolver” (Serve DNS names with CoreDNS ) remains a documented option.TailnetSite.spec.advertisedByis described as “any tagged router” — CRD description only, no schema change.
Known issues
- Ingress agents take a new tailnet identity on every restart (new IP, stale client DNS until the
30 s record TTL expires); tracked as issue
6929f01.
The docs/{INSTALL,USAGE,SECURITY,COMPATIBILITY}.md redirect stubs are removed in 0.19.0, as
announced in 0.17.0.
[0.17.0] - 2026-10-16
Changed
- Docs restructured along Diátaxis
:
docs/tutorials(getting started),docs/how-to(one task per page, split fromINSTALL.md/USAGE.md),docs/reference(generatedcrds.mdandvalues.mdviatask docs,rbac.md,compatibility.md,gateway.md, curatedmetrics.md/alerts.md/reasons.md),docs/explanation(model.md,security-model.md),docs/operations(keys, state and backup, validation).docs/OPERATIONS.mdkeeps the alert runbooks under unchanged anchors (alertrunbook_urls stay valid). The README CRD field tables moved todocs/reference/crds.md.docs/{INSTALL,USAGE,SECURITY,COMPATIBILITY}.mdremain as redirect stubs and are removed in the release after next. Documentation is licensed CC BY 4.0 (docs/LICENSE); the code stays Apache-2.0. deploy/examples/*.yaml: comments wrapped to 72 columns and theirUSAGE §/INSTALL §/README pointers replaced by the Diátaxis how-to pages. The YAML documents are unchanged.
[0.16.0] - 2026-10-14
Added
- Chart:
networkPolicy.cilium.headscalePeersandnetworkPolicy.headscaleCIDRsmay now coexist underprovider: cilium. The template adds atoCIDRegress forheadscaleCIDRsonheadscalePorts, so a cluster whose Headscale control plane lives outside the cluster (shared/spoke clusters) is covered by the chart’s own CiliumNetworkPolicy and no longer needs a side-car CNP. A fail guard rejectsprovider: ciliumwith both blocks empty. Default values keep the in-cluster label peer andheadscaleCIDRs: [], so hub clusters (hydra) render byte-identical specs on upgrade — verified intest/chart.
Fixed
- Gateway metrics stay observable across an operator restart.
Metrics.Observeis now called on every reconcile (not only when the status actually changed), so after a fresh Prometheus registry thetailnet_resource_ready{kind="TailnetGateway",…}andtailnet_resource_condition{…}gauges reappear without waiting for the next real status event. Observe is idempotent under the Metrics mutex, and suspended gateways still withdraw their series. Covered by the unit testTestGatewayMetricsSurviveRestartand the envtest subtestTestGatewayFreshWriter/metrics survive operator restart in fresh path(both red before, green after).
[0.15.1] - 2026-10-14
Changed
TailnetGateway.spec.routes.lan[]accepts up to 128 prefixes (was 32), so a single gateway can advertise a LAN plus the dozens of single-host site routes a corporate fabric fans out, without route aggregation the Headscale policy forbids.
[0.15.0] - 2026-10-14
Added
TailnetGateway.spec.dns.extraZones[]is rendered: each zone becomes a CoreDNShosts … { fallthrough }cache 30+forward . <upstreams>block next to the strict primary zone, for legacy networks whose internal resolvers carry authoritative overrides. Hosts-source names inside an extra zone are written to a third-corednsConfigMap keyhosts(live-reconciled and fingerprint-excluded likezone.db) and answered locally (A/AAAA); everything else, including non-address types for a known name, is forwarded — the documented weaker hosts+forward contract, opted into per zone. The renderer enforces non-overlapping zones, 1–8 unique canonical unicast upstreams and never the gateway’s LB IP. A Gateway without extra zones renders byte-identical to 0.14.x (same keys, content hash and Pod template), so upgrading does not touch its ConfigMap or serial.task test-gateway-dnscovers the rendered block against real CoreDNS 1.14.6 (local override, forward, NXDOMAIN, TXT fallthrough, hosts reload without restart, strict primary zone).
Changed
- Alert
runbook_urlannotations point todocs/OPERATIONS.md#monitoring-*; USAGE §10–14 (keys, diagnosis, upgrade, state and backup, validation, alert runbooks) moved there unchanged.
Documentation
TailnetGatewayis documented end to end: README model bullet, field table and status row; INSTALL §7a (prerequisites, order, afterwards); USAGE §9b worked example (gateway, LB pool, iBGP, split-DNS, cluster CoreDNS forward,TailnetSite, checks, change classes, fields the writer rejects).deploy/examples/tailnetgateway.yamlis that manifest withsuspend: true; gateway.md drops its planning-phase wording.docs/README.mdindexes every page with reading paths by role; USAGE §12a lists what the operator persists, what must be backed up and what happens while the operator is down.- The chart README’s permissions paragraph describes the shipped RBAC (no CR spec writes, gateway
patchonly for the teardown finalizer) instead of a planned DNS claim history; stale0.7.1version pins updated; the gateway is described by its scope (fresh-only, no adoption, IPv4) rather than as “experimental”.
[0.14.2] - 2026-10-13
Added
networkPolicy.provider: ciliumrenders an egress-onlyCiliumNetworkPolicy(toEntities: kube-apiserver, label peers, no CIDRs) in place of the ipBlock-based NetworkPolicy;kubernetesstays the default.TailnetGatewayconditions are exported throughtailnet_resource_ready/tailnet_resource_condition(suspended gateways are withdrawn), and the chart alertTailnetGatewayFreshBlocked(warning, 5 min) pages when the fresh writer blocks — follow-up to the 2026-10-07 expired-key incident (Issuecdcfb19).
Changed
task e2e-fullruns the CI gate (hack/ci-test.sh) followed by every Tier-2 kind suite; the dev shell now shipsgovulncheck, andhack/ci-test.sh/hack/e2e-kind.shhonour a pre-setGOTOOLCHAIN(CI keepslocal). No runtime change.
[0.14.1] - 2026-10-13
Fixed
TailnetSite: an exit node’s default routes (0.0.0.0/0,::/0) no longer count as anUnmanagedOverlap— they offer the internet, not a subnet. Before, every site on a tailnet with an exit node reportedUnmanagedandTailnetUnmanagedRouteOverlapfired after 30 min.- Metrics:
Readywith reasonRouteConflictis exported as such instead ofOther. PolicyRenderednames blocked sites (site/<name>) in its skipped list wording.
[0.14.0] - 2026-10-12
Added
TailnetSite(cluster-scoped): declare a remote network — branch/24, management range or a single host as/32— advertised by routers identified by tag (advertisedBy.tags, defaultpolicy.routeApprovers). The policy controller appendsautoApprovers.routesfor the prefixes, the existing replay approves them on every node carrying an advertiser tag (including a hand-built router in another cluster), and the site reportsAdvertised,Approved,Readyfrom the node list. Grants are not rendered: who may reach a site stays in the base policy.Tailnet.spec.reservedNetworks[]: address space no site may overlap (node prefix, each cluster’s LAN/pod/service/LB ranges). The pure registryinternal/routes.Buildblocks a site that overlaps a reserved network (RouteConflict/OverlapsReserved) or an older site (OverlapsSite; the older site is never affected), and reports — never blocks — a node without an advertiser tag that advertises an overlapping route (UnmanagedOverlap). Blocked sites are listed assite/<name>inPolicyRendered’s skipped inputs.- Alerts
TailnetRouteConflict(warning, 15 min) andTailnetUnmanagedRouteOverlap(info, 30 min); dashboard row “8. Routes”;tailnet_resource_ready/_conditioncarrykind="TailnetSite". Runbooks in USAGE §9a and §14. RBAC: readtailnetsites, write their status. - Without
TailnetSiteCRs the rendered policy is byte-identical to before, so upgrading never triggers a policy write.
Changed
- Pre-auth keys are bound to the identity they were minted for: the authkey Secret carries
tsunaro.io/server,tsunaro.io/tagandtsunaro.io/tailnet-uid. A key is reused only while its server and effective tag match the CR; a changed server or tag mints a new key at once and the agent pod restarts so the node re-registers with the new identity (the tag is now a pod-template annotation; the Tailnet UID is recorded only). Legacy and transferred Secrets without a binding keep their key and are labelled in place —TestTransferredKeysReuseCredentialsstill holds. Upgrading rolls every agent pod once because the pod template changed.
Fixed
After
PolicyApplied=False/PolicyRejectedthe write is retried at most every minute (was: up to 5 min). The blocker typically clears on the Headscale side — duplicate users deleted, a short-lived testsrcre-registered — without any Kubernetes event, so only the retry cadence decides how fast the live policy catches up. Transport failures (ApplyFailed) keep the 5-min cap.Test flake
TestVirtualHostReloadFencesDelayedSNIMetrics(cmd/agent): the metric snapshot could race the new generation’s own byte accounting, which the HTTP listener records afterConn.Writereturns. The test now closes its keep-alive connection and waits for the connection gauge to drop before snapshotting; the fencing assertion itself is unchanged. No runtime change.
[0.13.0] - 2026-10-09
Added
TailnetconditionUsersUniqueand gaugetailnet_headscale_user_conflicts(status.userConflicts): the operator lists Headscale users every reconcile and reports names shared by more than one user. Headscale rejects every policy write while such a collision exists (multiple users found), which used to surface only asPolicyApplied=False/PolicyRejectedwith a bare HTTP status; that message now points atUsersUnique. Only the count and the user IDs are published, never a name. The check is informational and does not block writes.- Dashboard: row 2 gains a stacked bar chart of resources per active
Readyreason, row 3 a history of egress targets inNotApproved.
Changed
Ready=FalseonTailnetEgressandTailnetIngressnow repeats the reason and message of the first blocking condition (egress:Registered,RoutesGranted,TargetsReachable; ingress:Registered,PolicyGranted,BackendsReachable) instead of the genericNotReady, so theTailnetResourceNotReadyalert label and the dashboard name the failure class.NotReadyremains only before the first complete reconcile.TailnetGatewaykeepsWorkloadCurrentasUnknown/Suspended,Unknown/RuntimeUnavailableorUnknown/TearingDowninstead of removing the condition; a suspended or draining gateway now shows seven conditions.- The agent no longer defaults
AGENT_MODEtoegress. An unset or unknown value is a startup error, and the egress Deployment now setsAGENT_MODE=egressexplicitly, as the ingress Deployment always did. An ingress agent that lost the variable used to come up silently as an egress; it now fails loudly. Upgrade the operator before or together with the agent image: an older operator renders egress Deployments without the variable, and this agent refuses to start from them. The chart ships both at matching versions. - The post-publication image audit (
hack/audit-image.sh) pins the tools the CI broker v1.4.1 ships: syft 1.54.1, trivy 0.75.0, andcosign-legacyv2.5.0 for every signature and attestation operation. The broker now dual-signs with cosign v3 (OCI referrers bundles) and the legacy tag form; the audit stays on the legacy binary because that is the form the registry’s trust extension and Kyverno verify. Everymainbuild since 2026-10-08 had failed at the audit’s tool preflight after the broker upgrade; those commits were published without SBOM or provenance attestations. The same broker also writesartifactType(image config) and empty base-image annotations into every index descriptor; the audit’s index contract now accepts exactly that and still rejects any other artifact type or key.
Security
- Go 1.26.9 and
golang.org/x/netv0.60.0:govulncheckreported ten reachablenet/http,net/textprotoandcrypto/tlsadvisories (GO-2026-6603 … 6617) against Go 1.26.7 / x/net v0.58.0, on the agent’s TLS and health paths among others. The module now requires Go 1.26.9, the CI test image is pinned to it, and the scan is clean with the fixed toolchain. The single remaining finding is the unfixedx/crypto/openpgpadvisory (GO-2026-5932) in a module we require but never import.
Documentation
- Gateway reference: when to set
routes.advertiseEgressTargets: false, why the DNS zone’sNS/SOA primary point at the apex itself, and that container images are the only mutable part of the gateway Pod template. status.dnsName/status.hostsare gateway-zone names rendered from the CR name and do not followspec.hostname;status.hostnameis the MagicDNS identity. INSTALL and USAGE now state that theingressNameTemplatezone must differ from the MagicDNSbase_domain, and the Kind rename test pins the invariant instead of recording it as a divergence.
[0.12.1] - 2026-10-07
Fixed
- A fresh
TailnetGatewayfell into permanentReady=False/FreshBlockedexactly 24 hours after creation: completed-set observation re-validated the minted pre-auth key’s expiry (gatewayKeyLifetime) and rejected the spent key. While blocked, the live DNS reconciler (UpdateDNSRevision) did not run, so the gateway’s zone kept stale agent addresses after ingress restarts. Expiry is now enforced only on the bootstrap paths (mint, record write, record recovery); observation of a set with a completion receipt still binds Secret name, UID, resourceVersion, key ID and the recorded expiry value, but no longer compares that value against the clock. No re-mint, no Headscale write, no CRD change.
[0.12.0] - 2026-10-07
Release notes
The generated ingress agent is now named ingress-<CR name> (Deployment and
NetworkPolicy), matching its Headscale host. A TailnetIngress may therefore share
its name with a user workload in the same namespace; before, such a CR stopped with
HTTPTransitionBlocked. On upgrade the operator drains and deletes the legacy
Deployment and NetworkPolicy named like the CR (only if the CR controls them; a
foreign object of that name is left alone) before the new agent starts, so every
ingress restarts once (Ready=False/WorkloadMigrating, normally under a minute).
Pod labels, ConfigMap, Secrets and the Headscale node identity are unchanged.
A TLS Secret that cert-manager issued for this CR’s own Certificate name and that
nothing owns any more (the leftover of a deleted CR) is taken over instead of
blocking the new CR with TLSSecretConflict.
The DNS printer column of TailnetIngress is replaced by HOSTS
(status.hosts), the names the agent actually serves: HTTP expose names and
aliases, or the node name for L4-only ingresses. status.dnsName stays the node’s
identity and first certificate name; it is not a route once every HTTP expose has a
dnsLabel. A request to a certificate name without a route now completes the TLS
handshake instead of ending in an alert and gets 308 to the only served host or
404 listing the served hosts (SNI and Host must still agree; unknown names are
still rejected).
monitoring.podMonitor.scrapeFrom (NetworkPolicy peers) is passed to the operator as
AGENT_SCRAPE_PEERS and adds one ingress rule on the agents’ health/metrics port to
every generated ingress and egress NetworkPolicy, replacing site-owned,
CNI-specific policies for Prometheus. Default: none.
Added
TailnetIngress.status.hostsand theHOSTSprinter column (replacesDNS).TailnetGatewayconditionServiceReady(True/LoadBalancerAssigned,False/LBPending);Readydoes not wait for it.- Event
GatewayTeardownFinishedwhen a gateway teardown releases its finalizer. - Chart value
monitoring.podMonitor.scrapeFrom, envAGENT_SCRAPE_PEERS.
Changed
- Ingress agent Deployment and NetworkPolicy are named
ingress-<CR>; the legacy objects are retired automatically (WorkloadMigrating,WorkloadMigrationBlocked). - Orphaned cert-manager TLS Secrets of the same Certificate name are adopted.
- The agent answers certificate names without a route with 308/404 instead of a TLS alert.
[0.11.0] - 2026-10-06
Release notes
A TailnetGateway static DNS record can now be a CNAME inside the same zone:
dns.staticRecords[] admits an optional target FQDN as a mutually exclusive
alternative to address. The target must lie within dns.zone (equal to the
zone or ending in .<zone>) and must differ from the record’s own FQDN. The
rendered Corefile also enables the CoreDNS prometheus :9153 listener and a
global reload plugin so that Corefile changes land without a Pod restart,
and the tailscale sidecar is now started with TS_ENABLE_METRICS=true on the
existing TS_LOCAL_ADDR_PORT (tcp/8080). The default chart’s PodMonitor
scrapes both ports. The completion receipt remains format 3; the Pod template
and the derived Corefile are now reconciled live, keyed off the operator-
written tsunaro.io/template-sha256 annotation on the StatefulSet
(StatefulSet fields outside the template, like replicas, selector,
serviceName and updateStrategy, are still bound). Existing fresh gateways
upgrade in place. A v0.10.0 completion receipt stores the StatefulSet and
Corefile digests under the old rule; on the first reconcile after upgrade the
operator recognises the old rule, re-verifies the whole retained set and rewrites
the immutable -fresh receipt to the new digests. The rewrite is crash-safe
through a transient -fresh-next staging ConfigMap: if the operator dies
between deleting and recreating -fresh, the next reconcile recovers from the
stage instead of minting a new identity. Afterwards the operator (idempotently)
rewrites the Pod template with the new metrics env/port; the running Pod picks up
the new Corefile via reload 1s, and you delete Pod <name>-0 once to also
roll the sidecar onto TS_ENABLE_METRICS. A format-1/2 receipt still reports
IdentityChanged and must be recreated.
No CRD, RBAC or chart contract change beyond the additive target field.
Added
GatewayStaticRecord.target: mutually exclusive withaddress, admits an FQDN inside the samedns.zoneand renders a CNAME inzone.db. CRD CEL rejects both-set, neither-set, out-of-zone and self-FQDN targets at admission.- Operator metrics coverage for
TailnetGatewaypods: Tailscale sidecar Prometheus endpoint on tcp/8080 (TS_ENABLE_METRICS=true), CoreDNS Prometheus endpoint on tcp/9153, both scraped by the defaultPodMonitor.
Changed
- The completion receipt no longer binds the
StatefulSetPod template or the derivedCorefile; both are reconciled live. Template drift is tracked through atsunaro.io/template-sha256annotation on theStatefulSet(annotation-keyed CAS),Corefiledrift through the existingtsunaro.io/dns-content-sha256annotation on the-corednsConfigMap.WorkloadCurrentnow readspod.labels[controller-revision-hash]againststs.Status.UpdateRevisioninstead of comparing container images. - A v0.10.0 (format-3) completion receipt is migrated to the new digest rule in
place on the first reconcile, via a crash-safe
-fresh-nextstaging ConfigMap; no gateway identity is reminted. Teardown also removes a leftover-fresh-next.
[0.10.0] - 2026-10-06
Release notes
A running fresh TailnetGateway now reconciles its route set and LoadBalancer pool
labels: changes to routes.lan, routes.advertiseEgressTargets, service.tailnetLBCIDR,
the targets of the Tailnet’s TailnetEgress objects and service.poolLabels reach the
StatefulSet template (TS_ROUTES) and the Service without reporting IdentityChanged.
The Pod is never restarted (OnDelete); WorkloadCurrent=False/PodTemplateStale names
the routes it does not advertise yet, and Ready binds to the running Pod’s own
TS_ROUTES. With an operator-owned policy, every gateway’s tags and advertised routes
are merged into tagOwners/autoApprovers.routes, so RoutesInSync turns True/Approved
without a manual headscale nodes approve-routes. The completion receipt is now format 3;
gateways with a format-1 or format-2 receipt report IdentityChanged and must be deleted
(spec.deletionPolicy: Delete) and recreated. The CRD remains experimental; spec.suspend
still defaults to true. No CRD, RBAC or chart contract change (Services and StatefulSets
already carry update).
Added
With an operator-owned policy (
policy.baseConfigMapset;Applywrites and replays approvals,DryRunonly renders), every freshTailnetGatewayof the Tailnet contributes its tags totagOwnersand its advertised routes (LAN,tailnetLBCIDR, egress host routes) toautoApprovers.routes, approved for the gateway’s own tags. The existing route-approval replay therefore approves gateway routes without a manualheadscale nodes approve-routes. Gateway create, change and delete trigger a re-render. BaseautoApproversentries stay and are merged by union.A running fresh
TailnetGatewaynow followsroutes.lan,routes.advertiseEgressTargets,service.tailnetLBCIDRand the targets of the Tailnet’sTailnetEgressobjects intoTS_ROUTESof the StatefulSet template, andservice.poolLabelsonto the LoadBalancer Service. The Pod is never restarted (OnDelete);WorkloadCurrent=False/PodTemplateStalenames the routes it does not advertise yet. New reasonsServiceUpdateBlockedandServiceUpdateConflict. The policy controller plans the same routes from the spec.
Changed
- The
TailnetGatewaycompletion receipt is now format 3: routes,tailnetLBCIDRandpoolLabelsare no longer pinned, andReadybinds to the running Pod’s ownTS_ROUTES. Gateways with a format-1 or format-2 receipt reportIdentityChanged; delete (deletionPolicy: Delete) and recreate them. TailnetGatewayRoutesInSyncnow reportsTrue/ApprovedorFalse/NotApproved(missing prefixes named) for a Ready fresh gateway instead ofUnknown/ExternallyManaged. It is read-only.
[0.9.0] - 2026-10-06
Release notes
A completed fresh TailnetGateway now tolerates spec updates: the completion
receipt (format 2) pins identity fields only, image/dns.image flow into the
StatefulSet template without a Pod restart (WorkloadCurrent reports staleness),
and a changed identity field reports Ready=False/IdentityChanged. The operator
Role gains update on statefulsets. Gateways created by 0.8.0 or earlier carry a
format-1 receipt and must be deleted (spec.deletionPolicy: Delete) and recreated.
The CRD remains experimental; spec.suspend still defaults to true.
Changed
- The
TailnetGatewaycompletion receipt is now format 2 and pins identity fields instead of generations and full spec hashes.deletionPolicy,suspend(including a suspend/resume round trip),dns.staticRecords,dns.wildcardAddress, container images and Tailnet fields other thanserveranddns.hostsConfigMapno longer block a running gateway. A changed identity field reportsReady=False/IdentityChangedinstead ofFreshBlocked. Gateways created by 0.8.0 or earlier must be deleted (deletionPolicy: Delete) and recreated. - The operator Role gains
updateonstatefulsets(operator namespace only). The StatefulSet digest in the receipt now excludes container images.
Added
imageanddns.imagechanges are written into the StatefulSet Pod template. TheOnDeletestrategy is kept, so the operator never restarts the Pod; the newWorkloadCurrentcondition reportsPodCurrentorPodTemplateStale(delete Pod<gateway>-0to apply).- Teardown completion is logged at
infolevel with the gateway name and outcome.
[0.8.0] - 2026-10-05
Release notes
Experimental fresh-only TailnetGateway continues from 0.7.1 with two steps toward
production use: an explicit teardown contract and a live DNS reconcile. The
TailnetGateway CRD remains experimental; spec.suspend still defaults to true
and existing gateways are not adopted. Gateways rendered by 0.7.x carry no DNS CAS
annotations and permanently report DNSReady=False/DNSUpdateBlocked; migrate by
deleting with spec.deletionPolicy: Delete and recreating.
Added
- Live DNS reconcile for a fresh
TailnetGateway: once the completion receipt is verified, the controller re-renders the derived-corednsConfigMap from the Tailnet hosts source on every reconcile, advancing the SOA serial only on semantic change and writing with a resourceVersion CAS. The initial render carries thetsunaro.io/dns-serialandtsunaro.io/dns-content-sha256annotations;zone.dbis excluded from the completion fingerprint whileCorefileand every other key remain digest-bound. TheDNSReadycondition reportsDNSInSync,DNSUpdateBlockedorDNSUpdateConflictand is independent ofReady. Gateways rendered by 0.7.x carry no annotations and permanently reportDNSUpdateBlockeduntil deleted and recreated withdeletionPolicy: Delete. TailnetGateway.spec.deletionPolicy: Deletetears down the operator-created fresh resources on CR deletion: StatefulSet first, then the Headscale node and the minted bootstrap key, then the receipt children by recorded UID and the state/auth Secrets and receipts last. Every delete is UID-preconditioned; foreign objects or Headscale errors block withReady=False/TeardownBlockedand nothing further is removed. Switching back toRetainreleases the finalizer.Retainstays the default and still deletes nothing.
Changed
- Chart RBAC: namespace-local
deleteon StatefulSets, PodDisruptionBudgets, ServiceAccounts, Roles and RoleBindings, andpatchontailnetgatewaysfor the teardown finalizer.
Fixed
- Headscale client: expire pre-auth keys by numeric ID as Headscale 0.26+ requires;
the previous
keyfield was silently ignored and expired nothing.
[0.7.1] - 2026-10-05
Fixed
- Isolate fixed DNS Service IPs from automatic LoadBalancer Service ClusterIP allocation in the Gateway API test fixture. A deterministic regression checks that separation; production reconciliation and provenance checks are unchanged.
Release notes
This release includes the experimental fresh-only Gateway described in 0.7.0 below. All experimental limits remain unchanged. The v0.7.0 tag is retained, but its failed release build published no image or chart; use 0.7.1 for installation.
[0.7.0] - 2026-10-05
Added
- Experimental, fresh-only
TailnetGatewayfor a persistent IPv4 kernel subnet router with a local authoritative DNS snapshot.spec.suspenddefaults totrue; explicitfalseenables create-only reconciliation. Existing gateways are never adopted. - Independently retained State/auth/child provenance, exclusive resource creation and runtime Ready checks against the persistent state and online Headscale identity. Partial or uncertain creation is retained and blocked, not reminted or repaired.
- Namespace-local
get/createfor StatefulSets, PDBs, ServiceAccounts, Roles and RoleBindings. No new update/delete, escalation or binding grants for these resources. - Real Kubernetes API tests and a controller-driven Kind proof with Headscale 0.29.3, Tailscale 1.102.4 and CoreDNS 1.14.6 on Linux amd64: fresh TCP /32 forwarding and local UDP/TCP DNS before and after StatefulSet Pod recreation, retaining node/IP/State UID. Backend socket-source checks distinguish forwarding from native Kubernetes reachability. Route absence before approval/after revocation is not packet-level ACL-denial evidence.
Experimental limits
Deliberately no adoption, IPv6, update or rollback path. Spec/generation changes and expired bootstrap keys keep blocking reconciliation.
In detail: no adoption, IPv6 writer, dynamic DNS/route updates, rollback/compensating cleanup, automatic key renewal, arbitrary topology proof, arm64 runtime or live deployment. Initial DNS stays at serial 1 and is never reset/reapplied. Expired auth blocks observation even if a retained router remains running. Spec/generation changes, including suspend/resume, invalidate completion evidence; general resume/lifecycle support is not implemented. Suspension freezes reconciliation, not the running workload. The immutable receipts are privileged Kubernetes trust anchors, not signatures or a transaction. No CR-deletion/GC execution proof is claimed.
Without update/delete permissions for the new workload and RBAC resources, a created Gateway is effectively immutable. Even an API-admitted spec change invalidates its completion receipt and blocks reconciliation; the controller cannot update or replace the retained runtime.
Ready proves bounded bootstrap/runtime health, not route approval, LoadBalancer allocation, DNS queries or cluster DNS forwarding. The Kind fixture invokes the production reconciler in-process; it does not prove a separately deployed operator or leader failover. Deployment compatibility on Talos/Cilium remains unverified. Keep an existing gateway and its state, routes and split DNS untouched; this release is not a cutover or migration mechanism. See the gateway reference for prerequisites and evidence bounds.
[0.6.0] - 2026-10-03
Added
- Virtual hosts: a
TailnetIngressmay publish severalmode: httpexposes behind one node. Each expose gets its own DNS name (spec.exposes[].dnsLabel, rendered throughTailnet.spec.dns.ingressNameTemplate); at most one HTTP expose may omit the label. The agent terminates TLS on a single certificate covering every name, routes by SNI, and requires the HTTPHostto equal the SNI. Resolved names appear instatus.exposes[].dnsName. - Optional short alias per HTTP expose (
spec.exposes[].alias) rendered fromTailnet.spec.dns.ingressAliasTemplate. The alias must resolve in the same DNS zone asingressNameTemplate; it becomes an extra certificate SAN and SNI route, reported instatus.exposes[].dnsAliases. - DNS names and egress aliases are arbitrated oldest-owner-wins across one Tailnet. A younger CR that
claims a name already held by an older one reports
Ready=False/DNSNameConflictand is not published, replacing the previous lexical hosts-file de-duplication. - Chart
agent.resourcesdefaults for ingress and egress: requests CPU10mand memory32Mi, memory limit128Mi, no CPU limit. Precedence selects one whole block: CRspec.resources>Tailnet.spec.proxy.resources> chart default, without per-key merging.agent.resources: nulldisables the chart fallback;{}retains Helm’s merged defaults. Direct binary runs withoutAGENT_RESOURCESretain the zero default.
Changed
- Upgrading to v0.6.0 retires every existing HTTP ingress agent once, including single-expose v1 and v2 ingresses, because the resolved TLS-source marker is absent. The operator scales the Deployment to zero and waits for its Pods to disappear before installing the current configuration. All exposes on that CR are briefly interrupted; L4-only ingresses do not use this HTTP barrier.
- Agents that inherit the new defaults restart once per CR with
Recreatewhen their effective resources change. Ephemeral nodes re-register; expect a brief interruption. Higher-tier overrides are preserved.
[0.5.1] - 2026-10-02
Fixed
- HTTP ingress no longer negotiates gzip or decompresses backend responses on the client’s behalf.
Stored object bytes, content-encoding metadata and explicit client
Accept-Encodingare preserved.
[0.5.0] - 2026-10-02
Added
- Verified HTTP-ingress backend TLS through
exposes[].backend.tls: explicit SNI/verification name and namespace-local CA ConfigMap key, TLS 1.2 minimum, no system-root or plaintext fallback. Backend readiness requires a fresh verified handshake and matching projected trust. - HTTP expose
timeouts.requestandtimeouts.backendRequest, with explicit0sto disable each timer. Omission preserves unlimited whole-response duration and the 30-second backend-header wait. - Agent configuration envelope v2 and
http-v2Pod protocol annotation for extended ingresses. Older agents reject it; new agents retain legacy L4 and v1 HTTP support.
Changed
- Backend CA updates replace transports and close idle connections without interrupting established streams. Missing or invalid projected trust blocks new backend traffic and readiness.
[0.4.0] - 2026-10-02
Changed
- Product name: Tsunaro. Go module
tsunaro.io/tailnet-operator. Chart name, image repository and binary names are unchanged. - Breaking: API group
tailnet.this-is-fine.io→tsunaro.io; annotation/label keys becometsunaro.io/<key>; the leader Lease istailnet-operator.tsunaro.io. There is no conversion: install the new CRDs, re-create the CRs under the new group, then delete the old CRDs. - Agent containers use a read-only root filesystem and no longer mount a ServiceAccount token.
- Headscale error bodies are withheld from logs, Events and status because they can echo credentials.
Added
- Helm chart (
charts/tailnet-operator) with generated standalone manifests underdeploy/, configurable agent image and namespace-local image-pull Secret references. - Leader election (default on), PodDisruptionBudget for
replicas > 1, topology spread settings. - Optional ServiceMonitor, PodMonitor, bundled Prometheus alerts and a Grafana dashboard (Grafana Operator or ConfigMap).
TailnetIngressHTTP mode: onemode: httpexpose on port 443 terminates TLS on the agent with akubernetes.io/tlsSecret (spec.tls.secretName), a cert-manager issuer (spec.tls.issuerRef) or the Tailnet-wide default (Tailnet.spec.defaults.tls.issuerRef). SNI and Host are bound to the ingress DNS name; the backend receives plain HTTP; certificates reload without a restart. New conditionTLSReady, status fieldstlsSecretName,tlsNotAfter,exposes[].tlsReady.- Metrics: per-target/per-expose state, Headscale request duration/count, policy write duration,
tailnet_policy_pending,tailnet_policy_generated, node online/last-seen, route replay counter, agent connection/byte/dial families. - RBAC:
Certificateget/create/patch/delete;finalizersupdate on the managed CRs. - Signed multi-arch images with SPDX SBOM and SLSA v1 provenance attestations;
govulncheckin CI. internal/migration: library helpers for explicit child ownership transfer (not wired into the controllers).- Security policy, RBAC reference, Renovate configuration.
[0.3.0]
- Headscale capability preflight: conditions
HeadscaleCompatible,PolicyMode,SplitDNS;COMPAT/DNScolumns; optionalheadscale.versionhint. File mode blocks policy writes and route-approval replay; bootstrap works against an empty policy database. - Breaking admission rules: Kubernetes ≥ 1.31;
specrequired; non-empty ingressallowFrom; stricter names, addresses and templates; unique target/expose names and expose ports; bounded list sizes and lifetimes. Validate existing CRs withtask test-validationbefore upgrading.
[0.2.3]
ApiReachablefollows Headscale/health(new reasonUnhealthy);ApiKeyValidfollows a live authenticated call (new reasonRejected); both re-checked at least every 5 min.
[0.2.2]
- The policy controller no longer re-reconciles on its own rendered ConfigMap write.
[0.2.1]
- Missed
autoApproversapprovals are replayed after every successful policy write/InSync check (eventRoutesApproved). Rejected or failed policy writes retry with backoff (15 s doubling to 5 min);TailnetPolicyApplyErrorstherefore counts more increments per outage.
[0.2.0]
- Breaking: API group
tailnet.fr0st.dev→tailnet.this-is-fine.iowithout conversion. - Reason
ProbeFailed;status.tagandTAGcolumn; ingress DNS names publish IPv4 only; MagicDNS name restored after a duplicate; transientApplyFailedlogged at info; status-only updates no longer re-trigger reconciles.
[0.1.1]
- Route diagnosis is prefix-aware: a target covered by an aggregate advertised route is
Present.
[0.1.0]
Tailnet.spec.policy(operator-owned policy,DryRun/Apply),TailnetIngress.spec.allowFrom, conditionsPolicyRendered/PolicyApplied/PolicyGranted, reasonsPolicyPending/PolicyDenied.
[0.0.6]
Tailnet.spec.dns,TailnetEgress.spec.targets[].aliases,TailnetIngress.status.dnsName,Backend.namespace.
Source: CHANGELOG.md at v0.18.0 — found a mistake? Tell us.