Skip to main content

Talos v1.14 Puts a BGP Speaker Inside the OS: What It Actually Deletes (Not MetalLB), and Who Can Actually Use It (Not Hetzner)

12 min readDora NodaDora Noda
Share
On this page

Talos v1.14.0-beta.1 landed on July 31, 2026 with a line that reads like a footnote: native BGP routing instances on the host, via an embedded GoBGP, configured with BGPInstanceConfig documents. Most people will skim it as "Talos ships MetalLB now."

It doesn't. MetalLB stays. Cilium's BGP control plane stays. What the feature actually removes from your cluster is something less visible and more valuable — the BGP session your physical routers hold with a Kubernetes pod. And before you plan around any of it: if your fleet runs on Hetzner Cloud, the cheapest place in Europe to run a self-hosted PaaS, you cannot use this at all, because nobody there will peer with you.

Here is the priced comparison first, then the wiring that actually works, then the prerequisite the release notes don't mention.


Four ways tenant traffic reaches a service IP, priced

Every self-hosted PaaS answers "how does a packet from the internet find a tenant's pod" with one of these. The numbers assume a small European fleet; substitute your own, the shape holds.

Talos native BGP (fronting an in-cluster speaker)MetalLB / Cilium L2 (ARP/NDP)Provider load balancerFloating IP / DNS-RR / tunnel
Hard prerequisiteAn upstream router that will peer with youA flat L2 segment where ARP is realA provider that sells oneA provider API, or Cloudflare/Tailscale
Cost, 1 external IP€0 marginal (see below)€0€7.49/mo (Hetzner LB11), $12/mo (DO HTTP), ~$16.43/mo + NLCU (AWS NLB)€0-ish, plus the controller you operate
Cost, 40 external IPs€0 marginal€0€59.92/mo packed 5-per-LB11, €299.60/mo at the hcloud-CCM default of one LB per ServiceLinear in floating IPs
Failure detectionBFD: 900 ms at the documented 300 ms × 3; ~450 ms at 150 ms × 3. Without BFD, up to the hold time (9 s in the docs' example)Gratuitous ARP: "within a few seconds"; MetalLB says file a bug above ~10 s, and clients with broken GARP handling stay blackholed until their cache expiresHealth-check driven: commonly 10–15 s interval × 3 retries → ~30–45 sProvider API call + ARP/DNS TTL
Ingress bandwidth ceilingN nodes, with multipath: true + maxPathsOne node's NIC — MetalLB calls this a fundamental limitation of ARP/NDPThe LB's tierOne node
Who holds the session to your routersThe machine confign/aThe providern/a
Cluster API fitA machine-config patch per poolA Helm valueA CCM annotationA controller + credentials

The three-line verdict:

  1. If you front everything with a single ingress controller, none of this matters. One Hetzner LB11 is €7.49/month. Talos BGP will save you €90 a year and cost you a router. Don't.
  2. It flips when tenants need their own routable IPs — raw TCP, per-tenant TLS at L4, dedicated addresses. At the hcloud cloud-controller-manager's default of one load balancer per Service, 40 tenant IPs is €299.60/month, €3,595/year, versus one more BGP UPDATE message. Somewhere north of about ten distinct external IPs the arithmetic stops being close.
  3. Neither matters if you can't peer. That gate belongs in this table, not in a footnote — see below.

The BGP column's "€0 marginal" is doing real work and deserves its own accounting. On Vultr, BGP sessions cost nothing; you peer with AS20473 and the only bill is the instance you were already renting. On OVHcloud, BGP Service is in alpha, explicitly not for production, and requires bare metal from the High Grade, Scale, or Advance Gen3 ranges with every server in the same 1-AZ region. In your own rack, "free" means an ASN through a sponsoring LIR, address space to announce, and a router you now operate — a few hundred euros a year on top of colo and transit you're already paying, plus a new on-call surface. It is genuinely marginal-zero per additional service IP, which is the axis that matters, and decidedly not zero to start.


What actually shipped, and the word "beta"

The v1.14 cycle carries DNS-over-TLS and DoH resolvers, btrfs, LVM volume groups and RAID, filesystem trim, noexec on /var by default for new clusters, and Flannel on an nftables-native backend. All fine. The BGP work is the interesting one.

BGPInstanceConfig is a named, repeatable machine-config document. The fields:

  • localASN, routerID, optional vrf — a routing instance bound to a Linux VRF, or the default routing domain.
  • advertise: []link names, not prefixes. Talos originates the addresses on those links as host routes (/32, /128). Put a VIP on lo and it gets announced.
  • neighbors[] — each with either address (numbered) or link (unnumbered, IPv6 link-local), peerASN, an optional per-neighbor localASN override, passive, holdTime, and an optional bfd block (transmitInterval, receiveInterval, detectMultiplier).
  • multipath / maxPaths — ECMP across neighbors.
  • installRoutes (default true) — when false, learned routes stay in that instance's BGP RIB and never reach the kernel FIB.
  • importRoutes[] — one-way import of selected prefixes from another named instance, re-advertised with this instance's own next hop.

Peer state surfaces as a BGPPeerStatus resource. Unnumbered sessions and RFC 8950 extended-next-hop are supported, so you can carry IPv4 prefixes over an IPv6-only peering. One documented caveat to file away: BFD only works in the default routing domain — GoBGP's embedded BFD listener is not VRF-aware.

Now the part to take seriously. This is v1.14.0-beta.1. The stable branch on August 4, 2026 was v1.13.8. BGPInstanceConfig is a machine-config document, which means its schema is versioned and validated — and also that it can still gain or rename fields between betas. Wire it up in a lab, write the patch, get your fabric peering rehearsed. Do not put tenant traffic on a beta-only data path for the sake of €90 a year.


The wiring that actually works

The instinct is to point Talos's BGP instance at your top-of-rack switch and be done. That works for advertising node loopbacks. It does not get you service IPs, because Talos has no idea which Service objects exist — that's LB-IPAM's job, and LB-IPAM lives in MetalLB or Cilium.

The architecture in Talos's own integration suite is a two-instance sandwich, and it's cleverer than it first looks:

One. A veth pair. One end stays in the host's default routing domain; the other gets attached to a VRF.

yaml
apiVersion: v1alpha1
kind: VethConfig
name: veth-speaker
addresses:
  - address: fda1:b2c3:d4e5:1::/127
peer:
  name: veth-router
  addresses:
    - address: fda1:b2c3:d4e5:1::1/127

Two. The VRF that isolates everything the in-cluster speaker tells us.

yaml
apiVersion: v1alpha1
kind: VRFConfig
name: vrf-workload
links:
  - veth-router
table: "89"

Three. The workload instance, which peers with the MetalLB or Cilium speaker running on this node. Note installRoutes: false — nothing it learns is allowed anywhere near the node's main routing table.

yaml
apiVersion: v1alpha1
kind: BGPInstanceConfig
name: workload
vrf: vrf-workload
localASN: 4200000002
installRoutes: false
neighbors:
  - address: fda1:b2c3:d4e5:1::
    peerASN: 4200000003
    holdTime: 9s

Four. The fabric instance — the only thing that talks to your routers. It imports a filtered prefix set from workload and re-advertises it upstream with its own next hop.

yaml
apiVersion: v1alpha1
kind: BGPInstanceConfig
name: fabric
localASN: 65001
advertise:
  - lo
multipath: true
maxPaths: 4
importRoutes:
  - bgpInstance: workload
    prefixes:
      - 198.51.100.0/24
neighbors:
  - link: enp1s0
    peerASN: 65000
    holdTime: 9s
    bfd:
      transmitInterval: 300ms
      receiveInterval: 300ms
      detectMultiplier: 3

Read the prefixes list again. That is an announce allowlist, enforced in the machine config, on a route that a Kubernetes workload cannot edit. A MetalLB misconfiguration — a fat-fingered IPAddressPool, a compromised operator, a tenant CRD that shouldn't have been reconciled — can now announce whatever it likes into workload, and the fabric instance will drop everything outside 198.51.100.0/24 on the floor. In the conventional setup, where the speaker pod peers with your top-of-rack switch directly, that same mistake is a routing incident on your fabric.

Which parts are lab and which are load-bearing? The veth pair, the VRF, installRoutes: false, and importRoutes are all production shape: that is the mechanism that keeps the tenant speaker's routes out of the node's main table and off your fabric session. What's specific to Talos's test harness is the other side of the fabric instance — in CI, enp1s0 faces a QEMU bridge running a synthetic CLOS fabric (talosctl cluster create --with-bgp-clos). In production, enp1s0 and enp2s0 are your real uplinks to two ToR switches, unnumbered, ECMP'd, and the ASNs are whatever your network team assigns.

One more detail worth stealing: Talos's Cilium test peers over an IPv6 /127 while advertising an IPv4 /32 VIP. That's RFC 8950 extended next-hop, and it means your underlay can be IPv6-only while tenant service IPs stay v4.


Verifying it, and backing it out

A walkthrough that never shows you how to tell it worked is half a walkthrough. Peer state is a COSI resource:

bash
talosctl get bgppeerstatus

IDs are <instance>/<peer-address>fabric/198.51.100.1, workload/fda1:b2c3:d4e5:1::. A session that never leaves a non-established state is almost always one of three things: peerASN doesn't match what the far side sends (set it to 0 to accept any ASN while you debug), the neighbor is configured passive on both ends so nobody dials, or you asked for bfd on an instance bound to a VRF — which, per the docs, is not supported.

Two assertions worth putting in your own smoke test, both lifted from Talos's suite: the service prefix should appear in the fabric's RIB and reach a different node, and it should be absent from the announcing node's main routing table — that's installRoutes: false doing its job. Rolling back is document removal: drop the BGPInstanceConfig, VRFConfig, and VethConfig documents and the links, the VRF, and the peer statuses all disappear. importRoutes can be surgically removed with a $patch: delete on that key alone, leaving the fabric session up.


The prerequisite nobody puts in the release notes

BGP takes two. You need an upstream router willing to accept your session, and that turns out to be the whole ballgame.

ProviderPeers with you?
Your own rack / coloYes — you own the router
VultrYes, free, via AS20473; bare metal and cloud instances
OVHcloudBGP Service, alpha, not for production; High Grade / Scale / Advance Gen3 bare metal, single 1-AZ region
DataPacket and similar bare-metal shopsYes, sessions on request
Hetzner CloudNo

Hetzner is the painful one, because it is exactly where cost-driven self-hosted PaaS fleets live. And the fallback isn't good either: MetalLB's own cloud-compatibility page explains that L2 mode is broken on Hetzner Cloud because ARP is emulated by the virtual network layer and only resolves IPs the platform itself assigned. You cannot grab a floating IP with a gratuitous ARP; you have to call the proprietary API, which MetalLB does not do. That's why hcloud-ip-floater-style controllers exist.

Talos v1.14 acknowledges this directly. Alongside BGPInstanceConfig it ships HCloudVIPConfig, which announces a VIP through the Hetzner Cloud API, and Layer2VIPConfig, which does gratuitous ARP. Both carry the same caveat in their documentation: control-plane nodes only, for the Kubernetes API server; any other use is unsupported. They are not a tenant-traffic answer. On Hetzner, the provider load balancer keeps winning by default — which is a fine outcome at €7.49/month for one ingress controller, and an expensive one at forty tenant IPs.


The thing it deletes today, on any fleet: your API-server load balancer

There is one use of native BGP that needs no MetalLB, no VRF, and no import filter, and it pays off immediately.

Give every control-plane node the same Kubernetes API VIP on lo, and let all of them announce it:

yaml
apiVersion: v1alpha1
kind: BGPInstanceConfig
name: fabric
localASN: 65001
advertise:
  - lo
multipath: true
maxPaths: 3
neighbors:
  - link: enp1s0
    peerASN: 65000
    bfd:
      transmitInterval: 150ms
      receiveInterval: 150ms
      detectMultiplier: 3

That's anycast. Three control planes, three equal-cost paths, upstream ECMP hashing across all of them, and BFD withdrawing a dead one in roughly 450 milliseconds. Compare the alternatives: Layer2VIPConfig and kube-vip in ARP mode both announce from one node at a time — a leader election, a single node's NIC, and gratuitous-ARP convergence measured in seconds. HCloudVIPConfig is a provider API call. A cloud LB in front of :6443 is another line item converging on a 30-second health check.

The config above is ten lines and lives in the machine config, which means it is applied by the same mechanism that installs the node — a Cluster API MachineDeployment patch or a control-plane template, versioned with everything else, no Helm release to reconcile and no chicken-and-egg where the thing that fronts your API server needs your API server to be scheduled.


What this means for a Cluster API fleet

Everything above is machine config, which is the good news: for a CAPI-managed Talos fleet, BGP configuration is a per-pool patch on TalosControlPlane / TalosConfigTemplate, versioned in the same Git repo as the rest of the cluster definition and rolled out by the same MachineDeployment mechanics. There is no separate networking release train.

There's a corollary, though. Machine config changes on Talos can require a node reboot depending on the field, and a rolling MachineDeployment update that touches BGP config will, by construction, drop and re-establish fabric sessions node by node. With BFD and ECMP that's a sub-second reconvergence per node — but only if multipath is on and your upstream actually installs multiple paths. Verify that in a lab before you roll it across a production pool, because a single-path fabric turns a rolling upgrade into a rolling outage.

And keep the honest verdict in view: unless you have both an upstream willing to peer and more than a handful of externally routable service IPs, the provider load balancer is still the right call. Native BGP in Talos isn't a way to stop paying €7.49 a month. It's a way to take the session your routers hold with a Kubernetes pod and move it into a file you version.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.


Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex