diff --git a/bastion/src/bastion/src/templates/install.ks.ts b/bastion/src/bastion/src/templates/install.ks.ts index e493320..e2725ac 100644 --- a/bastion/src/bastion/src/templates/install.ks.ts +++ b/bastion/src/bastion/src/templates/install.ks.ts @@ -372,6 +372,21 @@ fs.inotify.max_user_watches = 1048576 SYSCTL sysctl --system || true +# -- IPv6 link-local address generation: EUI-64, fleet-wide -- +# A cluster node takes its IPv6 from a MAC-keyed DHCPv6 reservation. kea can only +# match that reservation when it can recover the node's MAC, and for a modern +# client that sends a DUID-UUID (no MAC in it) the only place kea can find one is +# an EUI-64 link-local. NetworkManager's default is stable-privacy (RFC 7217), +# whose link-local hides the MAC -- so a node on the default silently never gets +# its reserved address, and with a reservations-only subnet it gets nothing at +# all. Proven on 2026-09-06: worker0/worker2 (eui64) bound; worker1/spark +# (default) did not, until flipped. Setting it here means a new node is correct +# from first boot, before its connection is ever activated. +cat > /etc/NetworkManager/conf.d/10-ipv6-eui64.conf << 'NMEUI64' +[connection] +ipv6.addr-gen-mode=eui64 +NMEUI64 + # -- Disable firewalld permanently (k3s/Cilium manage iptables directly) -- # Note: no '--now' — systemd is not running in the Anaconda chroot systemctl disable firewalld || true diff --git a/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts b/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts index b7f44f3..db16bed 100644 --- a/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts +++ b/bastion/src/bastion/src/templates/ubuntu-autoinstall.ts @@ -86,6 +86,11 @@ export function renderUbuntuAutoinstall(params: UbuntuAutoinstallParams): string `curtin in-target -- bash -c 'cat > /etc/modules-load.d/k3s.conf << EOF\nbr_netfilter\noverlay\nip_conntrack\nEOF'`, // Sysctl for k3s networking `curtin in-target -- bash -c 'cat > /etc/sysctl.d/90-k3s.conf << EOF\nnet.bridge.bridge-nf-call-iptables = 1\nnet.bridge.bridge-nf-call-ip6tables = 1\nnet.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\nfs.inotify.max_user_instances = 524288\nfs.inotify.max_user_watches = 1048576\nEOF'`, + // IPv6 link-local = EUI-64, so a MAC-keyed DHCPv6 reservation can match: kea + // recovers the node's MAC from an EUI-64 link-local when the client sends a + // DUID-UUID (no MAC in it). NM's stable-privacy default hides the MAC and the + // node silently never gets its reserved address. Proven 2026-09-06. + `curtin in-target -- bash -c 'cat > /etc/NetworkManager/conf.d/10-ipv6-eui64.conf << EOF\n[connection]\nipv6.addr-gen-mode=eui64\nEOF'`, // Disable ufw firewall `curtin in-target -- systemctl disable ufw || true`, // Enable chrony/ntp diff --git a/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts b/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts index 7fc1ed8..96e6e06 100644 --- a/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts +++ b/bastion/src/bastion/tests/ubuntu-autoinstall.test.ts @@ -67,6 +67,13 @@ describe("renderUbuntuAutoinstall", () => { expect(ids).toContain("mount-longhorn"); }); + it("sets EUI-64 link-local so MAC-keyed DHCPv6 reservations can match", () => { + const doc = parseYaml(renderUbuntuAutoinstall({ ...base, role: "worker" }), "eui64"); + const late = (doc.autoinstall["late-commands"] as string[]).join("\n"); + expect(late).toContain("10-ipv6-eui64.conf"); + expect(late).toContain("ipv6.addr-gen-mode=eui64"); + }); + it("requests both address families on the primary NIC", () => { const doc = parseYaml(renderUbuntuAutoinstall({ ...base, role: "worker" }), "net"); const eth = doc.autoinstall.network.ethernets.primary;