
Networking
MetalLB vs kube-vip: Bare-Metal Ingress with Gateway API
Run a Kubernetes bare-metal load balancer without a cloud provider — MetalLB BGP, kube-vip control-plane VIP, and Envoy Gateway API, exiting Cloudflare's proxy.
On a managed cluster, type: LoadBalancer is a comfortable fiction: four lines of YAML, the cloud-controller-manager provisions an external LB, an IP appears, traffic flows. The mechanism — anycast VIPs, edge network, DDoS scrubbing — is invisible and not yours. On bare metal the illusion collapses: a LoadBalancer service sits in <pending> forever. Most on-premises Kubernetes projects then discover that ingress was never solved — it was rented.
The sovereign replacement is not one tool but three mature open-source layers. This article wires them for production: MetalLB for service-IP advertisement, kube-vip for the control-plane VIP, and the Gateway API with Envoy Gateway for L7 routing that replaces Ingress annotations. Then the part most guides skip: walking off Cloudflare's orange-cloud proxy honestly — what you gain, what you own, and what you must not ignore.
Companion to the cloud repatriation playbook: repatriating compute is easy; replacing the cloud load balancer is what stalls migrations. Distinct from east-west Cilium and eBPF — this is the north-south path to the cluster edge.
Three Layers, Not One Tool: What Bare-Metal Ingress Actually Requires
The common mistake in on-premises ingress design is treating it as one problem. It is three, and conflating them yields two controllers fighting over one service IP. The concerns: (1) a control-plane VIP — a stable API-server address that survives losing any one control-plane node; (2) service IP advertisement — making a type: LoadBalancer address reachable on the physical network (ARP or a route); and (3) L7 routing — terminating TLS and dispatching by host and path. Cloud providers fuse these into one opaque product. On bare metal you assemble them — that assembly is the sovereignty.
The mapping is clean. kube-vip owns layer one. MetalLB owns layer two. A Gateway API implementation — Envoy Gateway or Contour — owns layer three. They compose because they act at different points in the packet's life: kube-vip and MetalLB put an IP on your network; the Gateway decides what to do with the bytes. The diagram shows the three planes inside one autonomous system — the mental model for the rest of this article.
kube-vip: The Control-Plane VIP That Bootstraps the Cluster
The control-plane VIP is the chicken-and-egg problem of self-hosted Kubernetes. Your kubeconfig and every kubelet point at one API-server address, but you have three control-plane nodes and no cloud load balancer in front of them. kube-vip runs as a static pod on each control-plane node and elects a leader through a Kubernetes lease; the leader owns the VIP. In ARP/Layer 2 mode, the leader emits gratuitous ARP so the switch fabric sends control-plane traffic to it; on failure the next node wins the lease within seconds. In BGP mode, each node peers with your top-of-rack routers and announces the VIP as a /32, so the routers ECMP across live nodes and reconverge on failure.
kube-vip is a common control-plane VIP choice on k3s and RKE2 (installed as a static pod or DaemonSet — neither distribution ships it by default) and is the default in several Cluster API infrastructure providers' templates, notably CAPV. Talos Linux does not use kube-vip: it has a native VIP arbitrated by etcd elections. A minimal ARP-mode manifest for a VIP at 10.0.0.10 looks like this. On Kubernetes 1.29+, bootstrap with /etc/kubernetes/super-admin.conf mounted first, then switch the hostPath to admin.conf once the cluster is up — admin.conf lacks coordination.k8s.io/leases during init:
# kube-vip static pod (control-plane VIP, ARP/L2 mode).
# Runs on every control-plane node; the lease leader owns 10.0.0.10.
apiVersion: v1
kind: Pod
metadata:
name: kube-vip
namespace: kube-system
spec:
hostNetwork: true # must see the host interface to send gratuitous ARP
hostAliases:
- ip: 127.0.0.1
hostnames: ["kubernetes"]
containers:
- name: kube-vip
image: ghcr.io/kube-vip/kube-vip:v0.8.9
args: ["manager"]
env:
- { name: vip_interface, value: "eth0" }
- { name: address, value: "10.0.0.10" } # the control-plane VIP
- { name: port, value: "6443" }
- { name: vip_arp, value: "true" } # ARP/L2 mode
- { name: vip_leaderelection, value: "true" } # HA via Kubernetes lease
- { name: cp_enable, value: "true" } # manage the control-plane VIP
securityContext:
capabilities:
add: ["NET_ADMIN", "NET_RAW"] # required to manage the interface + ARP
volumeMounts:
- mountPath: /etc/kubernetes/admin.conf
name: kubeconfig
volumes:
- name: kubeconfig
hostPath:
path: /etc/kubernetes/admin.confMetalLB: Advertising Service IPs to Your Physical Network
MetalLB makes type: LoadBalancer stop returning <pending>. It watches LoadBalancer services, allocates each an address from a pool you define, and advertises it in one of two modes. Layer 2 mode needs no router config: one elected node answers ARP (IPv4) or NDP (IPv6) for the service IP, so all traffic funnels through a single node before kube-proxy or Cilium spreads it. That dominates homelabs — but the single-node funnel caps throughput and makes failover an ARP-cache-timing question.
BGP mode is the production answer. Every node runs a MetalLB speaker that peers with your top-of-rack routers and announces each service IP as a /32. The routers ECMP-hash flows across advertising nodes, so bandwidth scales with node count and failover is routing reconvergence rather than an ARP gamble. As of MetalLB 0.16 (May 2026), FRR-K8s is the default BGP backend and the standalone frr mode is deprecated and slated for removal; the native speaker remains supported but is feature-limited (no IPv6 BGP, no BFD). FRR-K8s runs FRRouting as a managed backend and lets multiple controllers extend one FRR instance by submitting FRRConfiguration resources — so MetalLB's service advertisements can coexist with, for example, a route-receiving config you author yourself, on the same sessions. It does not merge with a CNI that runs its own BGP daemon: Cilium's BGP Control Plane uses GoBGP and Calico uses BIRD, and running either alongside MetalLB BGP mode means two speakers contending for TCP/179 — see MetalLB's Calico caveats page.
# MetalLB BGP mode with the FRR-K8s backend (MetalLB 0.16+).
# 1) The pool of externally-routable IPs MetalLB may hand out.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: prod-services
namespace: metallb-system
spec:
addresses:
- 198.51.100.16/28 # RFC 5737 documentation range; use your own routable block
autoAssign: true
---
# 2) The BGP peer — your top-of-rack router.
apiVersion: metallb.io/v1beta2 # v1beta1 BGPPeer is deprecated; use v1beta2
kind: BGPPeer
metadata:
name: tor-router
namespace: metallb-system
spec:
myASN: 64512 # the cluster's AS
peerASN: 64512 # iBGP here; use a different ASN for eBGP
peerAddress: 10.0.0.1 # ToR router address
# bfdProfile: sub-second failure detection, defined separately
---
# 3) Advertise the pool over BGP as /32 host routes (ECMP across nodes).
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
name: prod-advert
namespace: metallb-system
spec:
ipAddressPools: [prod-services]
aggregationLength: 32 # advertise each service IP as a distinct /32BGP mode also changes your security posture: nodes are BGP speakers, and a misconfigured or compromised node can advertise routes it should not. Constrain this with router-side prefix filters that accept only your allocated service block from the cluster's AS, BGP session authentication (TCP-MD5 or TCP-AO), and — with FRR-K8s — advertisement config in Git so announced prefixes are reviewable. That is the difference between a network you operate and one that operates you.
kube-vip vs MetalLB: Why You Often Want Both
kube-vip can *also* provide service load balancing, not just the control-plane VIP — so why run two tools? Because they optimize for different things. kube-vip's service LB is lightweight and convenient — ideal when the control plane and services share one simple network. MetalLB's BGP mode is the more capable service-LB engine: mature address-pool management, BFD, node selectors, and FRR-K8s's ability to merge additional FRRConfiguration resources onto the same FRR instance. The common production pattern: kube-vip for the control-plane VIP, MetalLB for the data-plane service IPs.
Gateway API vs Ingress: The Role Separation Worth Rewiring For
With a service IP reachable, the last layer is L7 routing, and the platform has moved. The Ingress API aged badly: every non-trivial capability — TLS options, rewrites, headers, canary weights — lived in controller-specific annotations, so manifests were portable in theory and locked to nginx-ingress or Traefik in practice. The Gateway API replaces it with typed, role-oriented resources. Core resources — GatewayClass, Gateway, and HTTPRoute — reached GA in v1.0 in October 2023; the current Standard channel is v1.6 (released 29 June 2026, with v1.6.1 on 16 July); v1.5 (27 February 2026) promoted six previously-experimental features to Standard (client-cert validation, TLS origination certificate selection, ListenerSet, HTTPRoute CORS, TLSRoute v1, plus ReferenceGrant to v1) and locked in the release-train cadence. Gateway API is the stable default; Ingress is legacy.
The reason to rewire is role separation. The GatewayClass is infrastructure — chosen once, like a StorageClass. A Gateway is platform-owned: listeners, ports, TLS certificates, bound to your MetalLB-advertised IP. HTTPRoute objects are app-owned: host/path matching and backends, with no privilege to change the listener or certificate. That is the golden-path model in the API — application teams route their own traffic without a platform ticket, and cannot misconfigure the shared edge.
# Gateway (platform-owned): binds to the MetalLB service IP, terminates TLS.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-edge
namespace: gateway-system
spec:
gatewayClassName: envoy-gateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: prod-tls # cert-manager-issued secret
allowedRoutes:
namespaces:
from: Selector # only namespaces the platform labels may attach
selector:
matchLabels: { gateway-access: "true" }
---
# HTTPRoute (app-owned): attaches to the Gateway, routes by host + path.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: checkout
namespace: shop
spec:
parentRefs:
- name: prod-edge
namespace: gateway-system
hostnames: ["shop.example.com"]
rules:
- matches:
- path: { type: PathPrefix, value: /checkout }
backendRefs:
- name: checkout-svc
port: 8080That HTTPRoute weighting also powers progressive delivery: shifting 5% to a canary is a backendRefs weight change the app team makes directly. TLS terminates at the Gateway against a cert-manager-issued secret, so your CA stays yours — pair with a sovereign internal PKI and no private key leaves infrastructure you operate.
Envoy Gateway vs Contour: Choosing the Data Plane
The Gateway API is a specification; you still choose an implementation to run the proxies. On bare metal the two mature CNCF-aligned Envoy-based choices are Envoy Gateway and Contour. Contour maps one controller to one fleet of Envoy instances, has a long track record as an ingress controller, and by v1.33 implements Gateway API v1.3 including TLSRoute, TCPRoute, GRPCRoute, and BackendTLSPolicy. It is the conservative, battle-tested pick, especially when migrating from an existing Contour Ingress deployment.
Envoy Gateway is the Envoy project's own Gateway API implementation, built on Envoy's xDS dynamic configuration, and the more forward-leaning choice. Its rate limiting is unusually complete — local and global (distributed, via the Envoy ratelimit service) limits, and since v1.5 monthly and yearly periods — expressed in BackendTrafficPolicy, which matters for the Cloudflare-exit story below. Contour also does local and global rate limiting, but through its own HTTPProxy CRD rather than a Gateway API policy attachment. Envoy Gateway's v1.5 release also cut memory roughly 25% by consolidating its xDS runners, and added an admin console, client-certificate validation in ClientTrafficPolicy, and zone-aware routing. For a greenfield sovereign edge where you want WAF-like controls and rate limiting inside the cluster rather than at a vendor's border, Envoy Gateway is the stronger default; Contour is the safer incremental migration. Either way the Gateway API resources you author are identical, so switching implementations is a gatewayClassName change, not a rewrite — the optionality is built into the standard.
Past Cloudflare: What You Gain, and What You Now Own
Exiting Cloudflare's proxy is where this becomes an engineering-sovereignty decision. When a record is proxied, resolvers get Cloudflare's anycast IPs; Cloudflare terminates TLS, runs its WAF, absorbs volumetric DDoS, caches assets, and hides your origin IP. It can also edit the response body — Scrape Shield rewrites email addresses in your HTML after your application has rendered it, which on this site broke React hydration on every page and corrupted published code samples. Flip to DNS-only and every function — and the traffic — reverts to you. Not a checkbox: a set of responsibilities you choose to repatriate.
Here is the honest reassignment. WAF and rate limiting move into Envoy Gateway: its declarative rate-limit CRD and SecurityPolicy cover the abuse controls Cloudflare's rules provided, and live in Git as reviewable YAML rather than a vendor dashboard. TLS moves to cert-manager at the Gateway, so you hold the private keys. Caching / CDN becomes an in-cluster concern — a Varnish or nginx layer, or a genuinely self-hosted CDN — right for dynamic and regional traffic, but no substitute for a global anycast edge serving a worldwide static audience. Volumetric DDoS is the one function you cannot fully self-host: absorbing hundreds of gigabits needs upstream capacity. The sovereign answer is transit-level scrubbing (Akamai Prolexic, or Cloudflare's BGP-based Magic Transit, which protects your prefixes without proxying HTTP) plus BGP remote-triggered blackholing with your upstreams — network engineering, not a toggle.
The Exit Ramp: Keeping Cloudflare Optional, Not Mandatory
The sovereign move is rarely to burn the bridge; it is to make it optional. Cloudflare stays a DNS provider — a commodity you can move in an afternoon — rather than a proxy welded into the path. Gateway API resources are vendor-neutral, so Envoy Gateway and Contour are interchangeable. MetalLB and kube-vip announce over standard BGP and ARP with no proprietary control plane. A scrubbing provider for a launch or attack window — Magic Transit or Prolexic — slots in at BGP without touching a Kubernetes manifest.
Contrast the start: type: LoadBalancer bound you to one cloud's controller and Cloudflare's proxy sat inline for every request. The sovereign stack still has dependencies — each is a component you chose, at a layer you understand, replaceable in isolation. That is what optionality means: not the absence of vendors, but the absence of vendors you cannot leave.
The Long Game: A Request Path You Can Read in Five Years
The deepest argument is legibility over time. When an engineer joins in three years and asks how a request reaches checkout, the answer is a readable chain: DNS resolves to a service IP; MetalLB advertises that /32 over BGP and the ToR routers ECMP it to a node; the node hands it to Envoy; an HTTPRoute matches host and path and forwards to the backend Service. Every hop is an object in Git or a route in a router config — inspectable, diffable, owned. No vendor console, no ticket to learn why traffic went somewhere, no opaque edge that changes when a provider ships a release you did not ask for.
That legibility is the compounding asset. The cloud repatriation case is usually argued on cost, and that case is real. The durable return is an ingress path your team understands and can operate for a decade without renting the parts that matter. MetalLB, kube-vip, and the Gateway API are not a downgrade from the managed load balancer you left — they are the same capability, disaggregated into layers you can see.
§FAQ/Common questions
Frequently asked
Can I run a Kubernetes bare metal load balancer without any cloud provider at all?
Yes. MetalLB provides the type: LoadBalancer capability that the cloud-controller-manager gives you on managed clusters, advertising service IPs to your physical network over Layer 2 (ARP/NDP) or BGP. Combined with kube-vip for the control-plane VIP and a Gateway API implementation for L7 routing, you get a complete ingress stack with no cloud dependency. All three are open-source and run entirely inside your own infrastructure.
Should I use MetalLB in Layer 2 mode or BGP mode?
Layer 2 mode needs no router configuration and works on any flat network, but funnels all traffic for a given service through one elected node, capping throughput and making failover depend on ARP cache timing. BGP mode has every node advertise service IPs as /32 routes to your top-of-rack routers, which ECMP-balance across nodes — so bandwidth scales with node count and failover is routing reconvergence. Use Layer 2 for homelabs and small clusters; use BGP mode with the FRR-K8s backend for production.
Do I need both kube-vip and MetalLB, or can one do everything?
kube-vip can provide both the control-plane VIP and service load balancing, and for a simple single-network cluster that may be enough. But MetalLB's BGP mode is the more capable service-LB engine — mature address-pool management, BFD, and FRR-K8s merging of additional FRRConfiguration resources onto one FRR instance. The common production pattern is kube-vip for the control-plane VIP and MetalLB for data-plane service IPs. If you run both, set spec.loadBalancerClass on your Services (Kubernetes 1.24+) so the two controllers do not fight over the same IP.
Is the Gateway API ready to replace Ingress on bare metal in 2026?
Yes. The core Gateway, GatewayClass, and HTTPRoute resources reached GA in v1.0 in October 2023, v1.5 (February 2026) promoted six more features to Standard and adopted a release-train cadence, and v1.6 (June 2026) is the current Standard channel (TCPRoute and UDPRoute graduated to v1). Mature bare-metal implementations exist — Envoy Gateway and Contour are both Envoy-based and CNCF-aligned. The role separation (platform owns the Gateway, apps own HTTPRoutes) and vendor-neutral spec make it the stable default; Ingress is now the legacy path.
What do I lose by exiting Cloudflare's proxy, and can I get it back in-cluster?
Proxying gives you edge WAF, rate limiting, caching/CDN, TLS termination, DDoS absorption, and origin-IP privacy. Going DNS-only, most of these move into the cluster: WAF and rate limiting into Envoy Gateway's SecurityPolicy and rate-limit CRDs, TLS into cert-manager, caching into an in-cluster layer. The two you cannot fully self-host are a global anycast CDN edge and volumetric DDoS absorption — for those, use transit-level scrubbing (Magic Transit or Prolexic) at the BGP layer plus RTBH with your upstreams. Origin-IP privacy is gone, so harden the edge with default-deny firewalling first.
Further reading
- CCPA compliance: deletion, opt-out and the 45-day clock
- When the edge edits your HTML: Cloudflare obfuscation and React #418
- KubeVirt: the VMware exit for VMs you cannot containerise
- Kubernetes upgrade debt: skew, deprecated APIs and safe paths
- Cloud repatriation: the Kubernetes on-premises engineering playbook
- eBPF and Cilium: zero-trust in-cluster networking without sidecars
- Talos Linux: the immutable Kubernetes OS security case
- Progressive delivery with Argo Rollouts: canary and blue-green via Gateway API
- Golden paths and policy enforcement: the platform-team model
- Internal PKI with step-ca and cert-manager: Private ACME
- Bare-metal Kubernetes provisioning: Metal3, Tinkerbell, Sidero, Omni
- The sovereignty thesis
- Infrastructure and platform capabilities
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.