Bare-Metal Kubernetes Networking with Cilium
On bare metal, Kubernetes has no cloud load balancer, no VPC-native pod routing, and often no automatic way to advertise LoadBalancer IPs. Cilium fills those gaps with an eBPF-based datapath, Kubernetes networking, service handling, optional kube-proxy replacement, load-balancer IP allocation, BGP or Layer-2 service advertisement, and identity-aware network policy. The key design decision is how external clients will reach Service IPs: Layer 2 announcements for a flat local subnet, or BGP for routed and larger environments.docs.cilium+1
Start by separating the address spaces:
-
Node subnet: Real IPs assigned to physical servers, such as
10.10.0.0/24. -
Pod CIDR: Addresses allocated to pods, such as
10.244.0.0/16. -
Service CIDR: Virtual Kubernetes ClusterIP range, such as
10.96.0.0/12. -
LoadBalancer IP pool: Addresses reserved for externally reachable Kubernetes Services, such as
10.10.0.200-10.10.0.220.
Traffic usually follows one of these paths:
-
A pod contacts another pod directly through Cilium’s datapath.
-
A pod or external client contacts a Kubernetes Service IP; Cilium selects a backend pod.
-
An external client contacts a LoadBalancer IP; a Cilium node advertises reachability through ARP/NDP (L2) or BGP routing, then forwards traffic to the service backend.
Do not overlap Pod, Service, LoadBalancer, node, VPN, or corporate network CIDRs. CIDR overlap causes routing ambiguity that can resemble random packet loss or failed service access.
Choose L2 or BGP
For a typical small on-premises cluster with nodes and clients on the same VLAN, L2 announcements are the simplest option. For your infrastructure-oriented use cases—multiple VLANs, edge firewalls, and routed corporate networks—BGP is usually the better long-term architecture because upstream routers learn where service IPs are reachable.
Install Cilium Safely
When using kube-proxy replacement, Cilium must know how to reach the Kubernetes API server without depending on the kube-proxy service path. Define the control-plane endpoint explicitly and roll out in a maintenance window.
Example Helm values:
kubeProxyReplacement: true
k8sServiceHost: “10.10.0.10”
k8sServicePort: 6443
ipam:
mode: kubernetes
l2announcements:
enabled: true
externalIPs:
enabled: true
devices: “eno1”
L2 announcements require kube-proxy replacement and require the announcing interface to be included in Cilium’s managed-device configuration if you set devices explicitly. Before removing or disabling kube-proxy, validate service connectivity, DNS resolution, NodePort behavior, and control-plane API reachability from every node. docs.cilium
Useful verification commands:
cilium status --wait
cilium connectivity test
kubectl -n kube-system get pods -l k8s-app=cilium -o wide
kubectl get ciliumnodes
kubectl get svc -A
Provide LoadBalancer IPs
Cilium LoadBalancer IPAM assigns an address from a defined pool to a Service of type LoadBalancer. Keep this pool outside DHCP ranges and do not use IPs assigned to nodes, printers, VPN clients, or other infrastructure.
apiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: lan-services
spec:
blocks:
- start: 10.10.0.200
stop: 10.10.0.220Next, tell Cilium which nodes/interfaces may announce those IPs on the LAN:
apiVersion: cilium.io/v2alpha1
kind: CiliumL2AnnouncementPolicy
metadata:
name: lan-lb-announcement
spec:
interfaces:
- eno1
loadBalancerIPs: trueThen expose an application:
apiVersion: v1
kind: Service
metadata:
name: demo-web
namespace: default
spec:
type: LoadBalancer
selector:
app: demo-web
ports:
- name: http
port: 80
targetPort: 8080Cilium’s L2-aware load-balancer capability makes services reachable on a local network without BGP routing, while Cilium’s IPAM feature supplies the address assigned to the Service. Verify the assigned IP with kubectl get svc demo-web, then test it from a host on the same VLAN using curl http://<external-ip> and inspect ARP with ip neigh or arp -an.
Cilium enforces standard Kubernetes NetworkPolicy resources for L3/L4 rules and extends policy options to Layer 7, allowing restrictions based on HTTP methods, paths, hosts, gRPC methods, Kafka topics, and DNS names. Begin with an explicit default-deny policy in one noncritical namespace, then add only required ingress and egress paths; applying cluster-wide default deny before allowing DNS, ingress, observability, and required dependencies can break workloads.
Example namespace default deny:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: apps
spec:
podSelector: {}
policyTypes:
- Ingress
- EgressAfter applying this, explicitly allow application-to-database traffic, ingress-controller-to-application traffic, and DNS egress to CoreDNS. Validate with a test pod rather than assuming the policy does what its YAML suggests.
When a bare-metal Service is unreachable, troubleshoot in this order:
-
Confirm the Service has endpoints:
kubectl get endpointslice -l kubernetes.io/service-name=demo-web. -
Test the Pod directly from inside the cluster.
-
Test the ClusterIP from a different Pod.
-
Test the LoadBalancer IP from a node, then from an external host on the same VLAN.
-
Check that the address lies in the Cilium IP pool and is not used elsewhere.
-
For L2, confirm the chosen interface, VLAN membership, ARP response, and switch port configuration.
-
For BGP, verify peer sessions, advertised prefixes, route acceptance, and return routing.
-
Inspect Cilium health and BPF/service state using cilium status, Hubble, and Cilium agent logs.
A strong first architecture exercise is to decide whether your external clients are on the same VLAN as the service IP range or reach it through a router. Describe that path—from a client IP to a Kubernetes Service—and identify whether L2 announcements or BGP would carry the service route.
[mai mult...]