Bird, BGP & Kubernetes
I’m still fiddling around with Kubernetes in my homelab, and this time I wanted to use BGP to announce addresses in my router, but had a hard time finding a working configuration for BIRD. This entry describes a basic but working configuration for use in a homelab, with MetalLB & BIRD.
Table of Contents
Why? #
For homelab usage, this is not any better than just using layer 2 advertisements with MetalLB. In theory, you can use BGP to loadbalance between nodes, but that comes with it’s own set of limitations, and since I don’t run my lab with any real high-availability ambitions I’m pretty sure I don’t really gain anything from this.
I just wanted to test BGP. ¯\_(ツ)_/¯
Overview #
I’m running BIRD2 on my home router1, which will establish BGP sessions with my Kubernetes nodes. Kubernetes will then announce IPs using said sessions, and BIRD will export these routes to the routers routing tables, allowing us to reach said IPs.
BIRD #
This configuration is typically located at /etc/bird/bird.conf
, but check
your distributions documentation for the correct location.
This configuration is also problematic, as it accepts any IP announced from our Kubernetes cluster. You can implement IP filters in BIRD2, I’ve just been too lazy.
Make sure to change:
router id
local as
, use a number from the private range, and change:- your router (one unique number),
65001
in the example below - your nodes (one number shared for all k8s nodes),
65010
in the example below
- your router (one unique number),
neighbor
IP addresses for your k8s nodes
# I use my routers kubernetes subnet IP for id
router id 192.0.8.1;
protocol direct {
}
# Export all routes from BGP to the kernel routing table, but don't export
# kernel routes back to BGP/kubernetes
protocol kernel {
ipv4 {
import none;
export all;
};
}
template bgp k8s {
local as 65001;
source address 192.0.8.1;
ipv4 {
# Import all routes from BGP, don't export any of our routes back
import all;
export none;
};
}
# Change peer addresses
protocol bgp node01 from k8s {
neighbor 192.0.8.10 as 65010;
}
protocol bgp node02 from k8s {
neighbor 192.0.8.11 as 65010;
}
protocol bgp node03 from k8s {
neighbor 192.0.8.12 as 65010;
}
You can print out the bird route table using ths following command, and once it’s working properly announced IPs should look something like this:
# birdc 'show route'
BIRD 2.17.1 ready.
Table master4:
192.0.8.102/32 unicast [node01 2025-07-10] * (100) [AS65010i]
via 192.0.8.10 on k8s
unicast [node03 2025-07-10] (100) [AS65010i]
via 192.0.8.12 on k8s
unicast [node02 2025-07-10] (100) [AS65010i]
via 192.0.8.11 on k8s
192.0.8.101/32 unicast [node01 2025-07-10] * (100) [AS65010i]
via 192.0.8.10 on k8s
unicast [node03 2025-07-10] (100) [AS65010i]
via 192.0.8.12 on k8s
unicast [node02 2025-07-10] (100) [AS65010i]
via 192.0.8.11 on k8s
192.0.8.100/32 unicast [node01 2025-07-10] * (100) [AS65010i]
via 192.0.8.10 on k8s
unicast [node03 2025-07-10] (100) [AS65010i]
via 192.0.8.12 on k8s
unicast [node02 2025-07-10] (100) [AS65010i]
via 192.0.8.11 on k8s
192.0.8.103/32 unicast [node01 2025-07-10] * (100) [AS65010i]
via 192.0.8.10 on k8s
unicast [node03 2025-07-10] (100) [AS65010i]
via 192.0.8.12 on k8s
unicast [node02 2025-07-10] (100) [AS65010i]
via 192.0.8.11 on k8s
MetalLB #
Below is a example configuration for announcing address pools using MetalLB and BGP. For me (using Talos) it wouldn’t work unless I added the namespace labels to make the namespace privileged, this is why it’s included here.
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
# These can be omitted if you don't follow security recommendations :)
labels:
kubernetes.io/metadata.name: metallb-system
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: privileged
---
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
name: example-bgp
namespace: metallb-system
spec:
# CHANGE ME
myASN: 65010
peerASN: 65001
peerAddress: 192.0.8.1
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: k8s-subnet
namespace: metallb-system
spec:
# CHANGEME
addresses:
- 192.0.8.100-192.0.8.200
autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: metallb-bgp-advert
namespace: metallb-system
spec:
ipAddressPools:
- k8s-subnet
Conclusion #
This works on my cluster(tm).
Which runs NixOSbtw, BIRD2 & nftables ↩︎