NetBird mesh DNS zone + apiserver cert SANs
Date: 2026-06-13 Status: Approved → implementing
Problem
kube-apiserver's serving cert only carries the control-plane endpoint host plus
kubeadm's defaults (node IPs, 10.96.0.1, kubernetes.default, …). A client
reaching the apiserver under a NetBird-mesh hostname hits an x509 name mismatch
— which is why kbm had to put a raw IP in its klist server:. We want:
- a predictable, per-cluster mesh name in every cluster's cert by default;
- an operator escape hatch (
extraCertSANs) for additional names that also works on bare-metal (today it only works on the hcloud LoadBalancer path).
Design
NetBird DNS zone — config + prompt
- New field
cluster.netbird.dnsZone. When empty, defaults (computed) to<cluster.name>.local— unique per cluster (e.g.kbm-obmondo-com.local). Because it's derived from the cluster name, the default lives in code (parser), not a static struct tag. cluster.netbirdbecomes valid for bothcluster.type: vpnandworkload. Itsdns/stun/turnsub-fields stay required only fortype=vpn(the cluster that hosts NetBird Mgmt). Socluster.typeis the gate —vpn= host + zone,workload= join + zone — with no extra "is netbird enabled?" boolean.- The prompt asks for the DNS zone (default
<cluster.name>.local) for both types;vpnadditionally collects the existing Mgmt-DNS / endpoint questions.
Caveat (accepted):
.localis reserved for mDNS (RFC 6762). On hosts running Avahi/Bonjour,kubernetes.<cluster>.localmay resolve via multicast instead of NetBird's unicast DNS. Operators who hit this set a non-.localzone in the prompt; the default stays.localper decision.
Usage of the zone
- Every cluster: apiserver cert gains the SAN
kubernetes.<dnsZone>. type=vpn(hosts Mgmt): NetBird Mgmt is configured with--dns-domain={{ .ClusterConfig.NetBird.DNSZone }}, replacing the hardcodednetbird.selfhostedatvalues-netbird.yaml.tmpl. One source of truth for the zone.
Cert SAN plumbing
extraCertSANsmade mode-agnostic — operator-supplied extra SANs work on every provider/mode, not just the hcloud LoadBalancer path it lives on today.pkg/core/templates.gobuilds the SAN list =[kubernetes.<dnsZone>]+extraCertSANs, emitted for all modes invalues-capi-cluster.yaml.tmpl.- kubeaid chart
KubeadmControlPlane.yaml: de-gate thecertSANsblock so it renders whenever any SAN exists (today it only renders whenendpoint.hostis set), letting thekubernetes.<dnsZone>default land on every cluster.
Repos
- kubeaid-cli (this branch): config field + computed default + prompt +
templates.goSAN list +values-*.yaml.tmpl(capi-cluster emission, netbird--dns-domainDRY). - kubeaid (chart, companion PR):
KubeadmControlPlane.yamlcertSANsde-gate.
Out of scope
- Retrofitting existing clusters' live certs (manual
kubeadm init phase certs apiserverregen; documented separately in the SAN troubleshooting). - Aligning klist's client-side
clusterPeerSuffixwith per-cluster.localzones (client-side; follow-up).
Revision (post-merge)
The <cluster-name>.local computed default was dropped. The DNS zone is
now operator-supplied only, collected via the prompt as a required
field with no pre-filled value (each NetBird mesh has its own zone; kubeaid-cli
shouldn't invent one). Consequences:
parser/netbird.gono longer defaultsdnsZone(theDefaultNetBirdDNSZonehelper is removed);hydrateNetBirdDefaultsis back to stun/turn only.templates.goaddskubernetes.<dnsZone>only whencluster.netbird.dnsZoneis set — a cluster with no netbird block / no zone gets nokubernetes.<zone>SAN (just its operatorcontrolPlane.extraCertSANs, if any).- The prompt asks the zone (required, example
mesh.acme.com) for both cluster types.