One PR, image and deployment together. The mesh fills kernel table 48; nothing reads it yet — the routing rules are a separate step, so a mistake here shows as an empty table rather than as broken pod traffic.
No address is written down twice
Each node publishes its pod0 addresses as Talos nodeAnnotations, beside the VLANConfig that assigns them. The init container reads the node list and renders from it.
bird.conf contains no address at all — and the pod CIDRs are not restated either. The own-CIDR filter keys on the outgoing interface plus a maximum-length exclusion, which separates the node's pod route from cilium_host's own host address without naming a prefix.
Annotations rather than labels: a label value cannot contain a colon.
Rendering, not string building
kubectl renders two go-template files. A field selector drops the node itself server-side. In-cluster config removes the token and CA plumbing. jq and curl are gone, and so is the shell script that assembled config text by hand.
kubectl comes from the Alpine package — no version literal to drift out of sync with the cluster, and no download URL to trust. It only ever reads core/v1 Nodes.
The init container ends with bird -p, so BIRD's own parser gates startup: a node whose annotation is missing renders as <no value> and fails there, rather than crashlooping the daemon or forming a mesh with a hole.
BIRD specifics that would bite silently
learn all, not learn — plain learn skips routes marked proto kernel, which Cilium's are.
direct; — iBGP defaults to multihop, which makes gateway recursive.
strict bind — keeps the listener off mgmt0 and ingress0.
Only BGP-learned routes are exported to table 48.
Verified against the live cluster
All three nodes annotated with --mode=try, then:
the templates rendered the expected defines.conf and four peer protocols, from the real API;
bird -p parsed the result inside the image, exit 0 — which also confirms net.len and the include ordering;
kubectl kustomize builds; the generator hash rolls the DaemonSet when any of the three files changes.
The try-mode annotations reverted themselves; the cluster is at baseline.
Not verified, because it is runtime behaviour: session establishment, route exchange, the table-48 writes.
Rollout
talosctl patch machineconfig the annotations, one node at a time — no reboot.
Merge; Flux applies the DaemonSet.
Per node: birdc show protocols has both peers up, talosctl get routes shows the peer CIDRs in table 48 via pod0.
One PR, image and deployment together. The mesh fills kernel table 48; **nothing reads it yet** — the routing rules are a separate step, so a mistake here shows as an empty table rather than as broken pod traffic.
## No address is written down twice
Each node publishes its `pod0` addresses as Talos `nodeAnnotations`, beside the `VLANConfig` that assigns them. The init container reads the node list and renders from it.
`bird.conf` contains no address at all — and the pod CIDRs are not restated either. The own-CIDR filter keys on the outgoing interface plus a maximum-length exclusion, which separates the node's pod route from `cilium_host`'s own host address without naming a prefix.
Annotations rather than labels: a label value cannot contain a colon.
## Rendering, not string building
`kubectl` renders two `go-template` files. A field selector drops the node itself server-side. In-cluster config removes the token and CA plumbing. `jq` and `curl` are gone, and so is the shell script that assembled config text by hand.
`kubectl` comes from the **Alpine package** — no version literal to drift out of sync with the cluster, and no download URL to trust. It only ever reads core/v1 Nodes.
The init container ends with `bird -p`, so BIRD's own parser gates startup: a node whose annotation is missing renders as `<no value>` and fails there, rather than crashlooping the daemon or forming a mesh with a hole.
## BIRD specifics that would bite silently
- `learn all`, not `learn` — plain `learn` skips routes marked `proto kernel`, which Cilium's are.
- `direct;` — iBGP defaults to `multihop`, which makes `gateway` recursive.
- `strict bind` — keeps the listener off `mgmt0` and `ingress0`.
- Only BGP-learned routes are exported to table 48.
## Verified against the live cluster
All three nodes annotated with `--mode=try`, then:
- the templates rendered the expected `defines.conf` and four peer protocols, from the real API;
- `bird -p` parsed the result **inside the image**, exit 0 — which also confirms `net.len` and the include ordering;
- `kubectl kustomize` builds; the generator hash rolls the DaemonSet when any of the three files changes.
The try-mode annotations reverted themselves; the cluster is at baseline.
Not verified, because it is runtime behaviour: session establishment, route exchange, the table-48 writes.
## Rollout
1. `talosctl patch machineconfig` the annotations, one node at a time — no reboot.
2. Merge; Flux applies the DaemonSet.
3. Per node: `birdc show protocols` has both peers up, `talosctl get routes` shows the peer CIDRs in table 48 via `pod0`.
Pod traffic rides `mgmt0` throughout.
Refs #576
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Nothing reads table 48 yet. The mesh fills it so the routing rules land on a table that is already correct, and so a mistake here shows as an empty table rather than as broken pod traffic.
No address is written down twice. Each node publishes its pod0 addresses as Talos nodeAnnotations, beside the VLANConfig that assigns them; the init container reads the node list and renders from it. bird.conf holds no address at all, and the pod CIDRs are not restated either — the own-CIDR filter keys on the outgoing interface plus a maximum-length exclusion, which separates the node's pod route from cilium_host's own host address without naming a prefix.
Rendering is go-template-file, not string building: kubectl renders the two templates, a field selector drops the node itself server-side, and in-cluster config removes the token and CA plumbing. kubectl comes from the Alpine package, so there is no version literal to drift out of sync and no download to trust; it only ever reads core/v1 Nodes. jq and curl are gone.
The init container ends with bird -p, so BIRD's own parser gates startup: a node whose annotation is missing renders as <no value> and fails there rather than crashlooping the daemon or forming a mesh with a hole.
learn all rather than learn, since plain learn skips proto kernel. direct is explicit because iBGP defaults to multihop, which would make the gateway recursive. strict bind keeps the listener off mgmt0 and ingress0.
Verified against the live cluster with all three nodes annotated via --mode=try: the templates rendered the expected defines and four peer protocols, and bird -p parsed the result inside the image with exit 0 — which also confirms net.len and the include ordering. kubectl kustomize builds.
Refs #576
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🤖Automated review — model CLI default (complexity: complex), commit 3a9ea5eefeced653d5a73cc338821a4d46ab1476.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
Adds a BIRD-based iBGP mesh over pod0 that mirrors each node's own pod CIDR (learned from the kernel/Cilium) into a BGP session with its peers and writes the result into kernel table 48. Config addresses come from per-node Talos nodeAnnotations rather than being duplicated in the repo, and are rendered into bird.conf/peers.conf by an init container using kubectl ... -o go-template-file. The PR body is explicit that table 48 is not yet consumed by any routing rule, so a mistake here is inert rather than traffic-breaking. I cross-checked the BIRD filters, the RBAC, and the rendering pipeline against the rest of the repo (Cilium's autoDirectNodeRoutes/directRoutingDevice: pod0/ipam.mode: kubernetes values, the pod0 VLANConfig addresses, and the existing CI pipeline that builds/pushes the bird image) and everything is internally consistent. No blocking issues found.
🟡 Suggestions
kubernetes/infrastructure/bird/daemonset.yaml:182-214 — the bird ServiceAccount (with get/list on nodes) is mounted into both the init container and the long-running bird container, but only the init container ever calls kubectl. The main container runs with NET_ADMIN and is the process actually exposed to the network (BGP on pod0); giving it a live API token as well is unnecessary attack surface. Consider automountServiceAccountToken: false at the pod level with the token instead projected explicitly into just the init container's volumes.
kubernetes/infrastructure/bird/daemonset.yaml:184-186 — the toleration only covers NoSchedule. multus-daemonset.yaml:193-197 and cni-plugins-installer.yaml:39-43 (this repo's other foundational networking DaemonSets) tolerate both NoSchedule and NoExecute; whereabouts-daemonset.yaml does not. If a node misses kubelet heartbeats, the node-lifecycle-controller adds a NoExecute taint and this pod would be evicted ~300s later (default tolerationSeconds) purely from an API-server-side node-health signal, unrelated to whether pod0 itself is still up — losing the BGP mesh at the moment a node is already flaky. Worth a deliberate decision either way, but given the comment "every node here is a control plane node, so this is not optional" the intent seems to be maximal placement, which argues for matching multus's broader toleration.
Tests
No automated tests apply to this kind of change (Talos machine config + BIRD config + a DaemonSet); the PR body documents manual verification instead: templates rendered from the live API with --mode=try, bird -p parsed the rendered config with exit 0 inside the built image, and kubectl kustomize builds. That covers syntax and rendering. It explicitly does not cover runtime BGP behavior (session establishment, route exchange, actual table-48 writes), which is reasonable to leave for the live rollout given table 48 is not yet consumed by anything — the stated rollout plan (birdc show protocols, talosctl get routes) is the right follow-up check post-merge.
Recommendation
Looks reasonable — no blocking issues; comments above are optional.
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `3a9ea5eefeced653d5a73cc338821a4d46ab1476`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
Adds a BIRD-based iBGP mesh over `pod0` that mirrors each node's own pod CIDR (learned from the kernel/Cilium) into a BGP session with its peers and writes the result into kernel table 48. Config addresses come from per-node Talos `nodeAnnotations` rather than being duplicated in the repo, and are rendered into `bird.conf`/`peers.conf` by an init container using `kubectl ... -o go-template-file`. The PR body is explicit that table 48 is not yet consumed by any routing rule, so a mistake here is inert rather than traffic-breaking. I cross-checked the BIRD filters, the RBAC, and the rendering pipeline against the rest of the repo (Cilium's `autoDirectNodeRoutes`/`directRoutingDevice: pod0`/`ipam.mode: kubernetes` values, the `pod0` VLANConfig addresses, and the existing CI pipeline that builds/pushes the `bird` image) and everything is internally consistent. No blocking issues found.
## 🟡 Suggestions
- `kubernetes/infrastructure/bird/daemonset.yaml:182-214` — the `bird` ServiceAccount (with `get`/`list` on nodes) is mounted into both the init container *and* the long-running `bird` container, but only the init container ever calls `kubectl`. The main container runs with `NET_ADMIN` and is the process actually exposed to the network (BGP on `pod0`); giving it a live API token as well is unnecessary attack surface. Consider `automountServiceAccountToken: false` at the pod level with the token instead projected explicitly into just the init container's volumes.
- `kubernetes/infrastructure/bird/daemonset.yaml:184-186` — the toleration only covers `NoSchedule`. `multus-daemonset.yaml:193-197` and `cni-plugins-installer.yaml:39-43` (this repo's other foundational networking DaemonSets) tolerate both `NoSchedule` and `NoExecute`; `whereabouts-daemonset.yaml` does not. If a node misses kubelet heartbeats, the node-lifecycle-controller adds a `NoExecute` taint and this pod would be evicted ~300s later (default `tolerationSeconds`) purely from an API-server-side node-health signal, unrelated to whether `pod0` itself is still up — losing the BGP mesh at the moment a node is already flaky. Worth a deliberate decision either way, but given the comment "every node here is a control plane node, so this is not optional" the intent seems to be maximal placement, which argues for matching multus's broader toleration.
## Tests
No automated tests apply to this kind of change (Talos machine config + BIRD config + a DaemonSet); the PR body documents manual verification instead: templates rendered from the live API with `--mode=try`, `bird -p` parsed the rendered config with exit 0 inside the built image, and `kubectl kustomize` builds. That covers syntax and rendering. It explicitly does **not** cover runtime BGP behavior (session establishment, route exchange, actual table-48 writes), which is reasonable to leave for the live rollout given table 48 is not yet consumed by anything — the stated rollout plan (`birdc show protocols`, `talosctl get routes`) is the right follow-up check post-merge.
## Recommendation
Looks reasonable — no blocking issues; comments above are optional.
<!-- pr-review:sha=3a9ea5eefeced653d5a73cc338821a4d46ab1476 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
One PR, image and deployment together. The mesh fills kernel table 48; nothing reads it yet — the routing rules are a separate step, so a mistake here shows as an empty table rather than as broken pod traffic.
No address is written down twice
Each node publishes its
pod0addresses as TalosnodeAnnotations, beside theVLANConfigthat assigns them. The init container reads the node list and renders from it.bird.confcontains no address at all — and the pod CIDRs are not restated either. The own-CIDR filter keys on the outgoing interface plus a maximum-length exclusion, which separates the node's pod route fromcilium_host's own host address without naming a prefix.Annotations rather than labels: a label value cannot contain a colon.
Rendering, not string building
kubectlrenders twogo-templatefiles. A field selector drops the node itself server-side. In-cluster config removes the token and CA plumbing.jqandcurlare gone, and so is the shell script that assembled config text by hand.kubectlcomes from the Alpine package — no version literal to drift out of sync with the cluster, and no download URL to trust. It only ever reads core/v1 Nodes.The init container ends with
bird -p, so BIRD's own parser gates startup: a node whose annotation is missing renders as<no value>and fails there, rather than crashlooping the daemon or forming a mesh with a hole.BIRD specifics that would bite silently
learn all, notlearn— plainlearnskips routes markedproto kernel, which Cilium's are.direct;— iBGP defaults tomultihop, which makesgatewayrecursive.strict bind— keeps the listener offmgmt0andingress0.Verified against the live cluster
All three nodes annotated with
--mode=try, then:defines.confand four peer protocols, from the real API;bird -pparsed the result inside the image, exit 0 — which also confirmsnet.lenand the include ordering;kubectl kustomizebuilds; the generator hash rolls the DaemonSet when any of the three files changes.The try-mode annotations reverted themselves; the cluster is at baseline.
Not verified, because it is runtime behaviour: session establishment, route exchange, the table-48 writes.
Rollout
talosctl patch machineconfigthe annotations, one node at a time — no reboot.birdc show protocolshas both peers up,talosctl get routesshows the peer CIDRs in table 48 viapod0.Pod traffic rides
mgmt0throughout.Refs #576
🤖 Generated with Claude Code
🤖 Automated review — model
CLI default(complexity: complex), commit3a9ea5eefeced653d5a73cc338821a4d46ab1476.Summary
Adds a BIRD-based iBGP mesh over
pod0that mirrors each node's own pod CIDR (learned from the kernel/Cilium) into a BGP session with its peers and writes the result into kernel table 48. Config addresses come from per-node TalosnodeAnnotationsrather than being duplicated in the repo, and are rendered intobird.conf/peers.confby an init container usingkubectl ... -o go-template-file. The PR body is explicit that table 48 is not yet consumed by any routing rule, so a mistake here is inert rather than traffic-breaking. I cross-checked the BIRD filters, the RBAC, and the rendering pipeline against the rest of the repo (Cilium'sautoDirectNodeRoutes/directRoutingDevice: pod0/ipam.mode: kubernetesvalues, thepod0VLANConfig addresses, and the existing CI pipeline that builds/pushes thebirdimage) and everything is internally consistent. No blocking issues found.🟡 Suggestions
kubernetes/infrastructure/bird/daemonset.yaml:182-214— thebirdServiceAccount (withget/liston nodes) is mounted into both the init container and the long-runningbirdcontainer, but only the init container ever callskubectl. The main container runs withNET_ADMINand is the process actually exposed to the network (BGP onpod0); giving it a live API token as well is unnecessary attack surface. ConsiderautomountServiceAccountToken: falseat the pod level with the token instead projected explicitly into just the init container's volumes.kubernetes/infrastructure/bird/daemonset.yaml:184-186— the toleration only coversNoSchedule.multus-daemonset.yaml:193-197andcni-plugins-installer.yaml:39-43(this repo's other foundational networking DaemonSets) tolerate bothNoScheduleandNoExecute;whereabouts-daemonset.yamldoes not. If a node misses kubelet heartbeats, the node-lifecycle-controller adds aNoExecutetaint and this pod would be evicted ~300s later (defaulttolerationSeconds) purely from an API-server-side node-health signal, unrelated to whetherpod0itself is still up — losing the BGP mesh at the moment a node is already flaky. Worth a deliberate decision either way, but given the comment "every node here is a control plane node, so this is not optional" the intent seems to be maximal placement, which argues for matching multus's broader toleration.Tests
No automated tests apply to this kind of change (Talos machine config + BIRD config + a DaemonSet); the PR body documents manual verification instead: templates rendered from the live API with
--mode=try,bird -pparsed the rendered config with exit 0 inside the built image, andkubectl kustomizebuilds. That covers syntax and rendering. It explicitly does not cover runtime BGP behavior (session establishment, route exchange, actual table-48 writes), which is reasonable to leave for the live rollout given table 48 is not yet consumed by anything — the stated rollout plan (birdc show protocols,talosctl get routes) is the right follow-up check post-merge.Recommendation
Looks reasonable — no blocking issues; comments above are optional.