Usman Khan

/Writing

Homelab Kubernetes Cluster

Homelab networking: the part everyone skips

· 8 min read

Isometric illustration of the homelab network: the internet feeds a home router that provides Wi-Fi to the house through three mesh units. An Ethernet run goes to an office switch with a desk, printer and Wi-Fi access point, then an uplink goes to a 10-inch rack holding a patch panel, a Netgear switch and five EliteDesk nodes, labelled cp1 to cp3 and wn1 to wn2 with their IP addresses.
Home, office, rack: the three hops from the internet to the cluster.

Networking is the part of a homelab build that nobody films. There’s no satisfying click of a node sliding into a rack, just a router admin page and a list of numbers. It’s also the part that’s hardest to change later.

In Kubernetes, and especially on the control plane, IP addresses get baked in: into certificates, into etcd’s member list, into every config file and client that talks to the cluster. Re-addressing a running control plane is painful. So before installing anything, I sat down and decided where every address would live.

What I started with

Nothing exotic. The cluster lives on my normal home network: one flat 192.168.1.0/24 subnet, shared with every phone, laptop and TV in the house, behind the ISP-supplied router (a Technicolor FGA5330CFL). The router does DHCP and DNS for everything.

The Technicolor router's admin dashboard, showing DHCP enabled, gateway 192.168.1.1 with netmask 255.255.255.0, and eight Ethernet devices connected
The router's dashboard. Gateway on .1, a /24 subnet, and DHCP doing the work.

Physically, it’s three hops from the router to the cluster:

  • Home: the router connects to the internet and provides Wi-Fi to the rest of the house through a mesh system.
  • Office: a single Ethernet run goes from the router to a TP-Link unmanaged switch in my office. That switch feeds the wired office devices and a separate Wi-Fi access point for the office.
  • Rack: one more cable goes from the TP-Link switch into the rack’s patch panel and on to the Netgear switch, which connects the five nodes through the same patch panel.
Network diagram in three zones. Home: the internet connects to the home router on 192.168.1.1, which provides Wi-Fi to the house through a mesh system. An Ethernet run goes to the office, where an unmanaged TP-Link switch feeds a desktop, a printer and an office Wi-Fi access point. An uplink goes to the rack: patch panel, Netgear GS308E switch on .100, then five nodes, cp1 to cp3 on .101 to .103 sharing the API VIP on .10, and wn1 and wn2 on .104 and .105.
The whole path, from the internet to the rack. Everything sits on one flat 192.168.1.0/24 network.

No VLANs, no separate lab network, no fancy firewall. That’s a deliberate starting point, not the end state. A flat network is the simplest thing that works, and I wanted the cluster running before adding layers. Separating the lab from the household is on the list for later.

The address plan

Here’s the whole plan in one table:

Address What lives there How it’s assigned
.1 Router (gateway and DNS) Router itself
.2–.9 Reserved for Kubernetes LoadBalancer services Static, outside DHCP
.10 Kubernetes API (floating virtual IP) Static, outside DHCP
.11–.15 Intel AMT remote management, one per node Static, outside DHCP
.16–.19 Spare Not yet used
.20–.254 DHCP pool for everything else in the house DHCP
.100 Netgear GS308E switch DHCP reservation
.101–.105 The five cluster nodes DHCP reservations

There are two kinds of address here, and the split matters.

Addresses that must never come from DHCP live below .20: the Kubernetes API’s floating address, the AMT management addresses and the range set aside for load balancers. These are claimed by the cluster or the hardware itself, not handed out by the router. If any of them were inside the DHCP pool, sooner or later the router would give one to somebody’s phone, and two devices would fight over the same address.

Addresses that come from DHCP, but always the same one are the switch and the nodes. They ask the router for an address like any other device, but the router has a reservation for each MAC address, so they always get the same IP.

Making room: moving the DHCP pool

Out of the box, the router’s DHCP pool ran from .2 to .254, which is almost the whole subnet. That left nowhere safe for static addresses.

So the first change was moving the start of the pool to .20. That carved out .2–.19 for the static addresses, with some headroom. I may widen that gap later if I need more static addresses, but it’s enough for now.

The router's DHCP settings: pool starting at 192.168.1.20 and ending at 192.168.1.254 with a 12-hour lease, above a list of static leases
The DHCP pool now starts at .20, leaving .2 to .19 free for addresses the router must never hand out.

Before moving the pool, I checked that nothing was already using an address below .20. Shrinking the pool doesn’t kick existing devices off their addresses straight away, but it does mean the router stops managing them. Better to know first.

Reservations for the nodes

The switch and the five nodes each have a DHCP reservation, tied to their MAC address.

A table of static DHCP leases on the router: ng-gs308e-switch on 192.168.1.100, and homelabs-k8-cp1 to cp3 and wn1 to wn2 on 192.168.1.101 to .105
The reservations. MAC addresses blurred; everything else is exactly as configured.

The numbering is deliberately boring:

  • .100 is the switch. I gave it a reservation so I can always find its management page.
  • .101, .102, .103 are CP1, CP2 and CP3.
  • .104, .105 are WN1 and WN2.

The last digit matches the node number, so I never have to look anything up. Starting at .101 also leaves a clean run of addresses for more nodes later.

Why reservations rather than static IPs set on each node? Three reasons:

  • One source of truth. Every address on the network lives in one place, the router, right next to every other device in the house. There’s no second list to keep in sync.
  • Simpler node configs. Each Talos node just asks for an address, so there’s no per-node network section to get wrong. Reinstalling a node needs no network changes at all.
  • The router knows they’re taken. With a reservation, the router will never hand that address to anything else. A static IP set only on the node is invisible to the router.

The trade-off is that the nodes depend on the router remembering those reservations. If the router were ever reset to factory settings, the nodes would come back on random addresses and the control plane wouldn’t find itself. That’s one reason these screenshots exist: they double as my backup of the router’s config. Setting static addresses in the Talos configs themselves, kept in Git, is the more “infrastructure as code” approach, and I may move to it once the cluster’s config is managed that way.

The floating API address

Kubernetes clients — kubectl, the worker nodes, anything else that talks to the cluster — need one address for the Kubernetes API. If that address belonged to a single control-plane node, losing that node would take the whole API with it, even with the other two healthy.

Talos solves this with a virtual IP. The three control planes share 192.168.1.10. At any moment one of them holds it, chosen through etcd. If that node goes down, another takes over the address, and clients carry on talking to .10 without noticing.

Two things follow from that:

  • It has to sit outside the DHCP pool. The router doesn’t know it exists, so it would happily give it to something else.
  • It depends on etcd. If the cluster loses quorum, nobody can win the election and the address disappears. That’s correct behaviour, but it surprised me the first time I saw it.

I got to watch the failover happen sooner than planned. That’s a story for a later post.

Two addresses per node

Every EliteDesk actually has two IP addresses on the same network cable: one for the operating system, from the reservations above, and one for Intel AMT, the remote-management controller built into the hardware. AMT addresses are set by hand in each machine’s firmware, so they live in the static range at .11–.15.

AMT is what lets me power-cycle a node and see its screen from my laptop. It gets its own post next.

Saving space for load balancers

On a cloud provider, creating a Kubernetes service of type LoadBalancer gets you an external IP automatically. On bare metal, nothing provides one, so the service sits at Pending forever.

The usual fix is MetalLB, which hands out addresses from a pool you give it. I haven’t installed it yet, but I’ve already set aside .2–.9 for it. Eight addresses sounds small, but most apps will sit behind a single ingress address, so it should be plenty.

The switch

The switch is a Netgear GS308E: eight gigabit ports, with a basic web interface for things like VLANs and port monitoring. For now I haven’t changed a single setting on it. Its only configuration is the DHCP reservation on .100, so I can reach its management page when I need it.

Five ports go to the nodes through the patch panel, one goes to the router, and two are spare. Gigabit is plenty for this cluster today. (Getting every node to actually run at gigabit was another matter. That’s in the remote management post.)

Check before you claim

The one habit I’d recommend from all this: before you reserve or claim an address, prove nothing else is using it.

Before setting the reservations for the switch and nodes, I went through the router’s device list to make sure none of .100–.105 was already in use. I did the same before moving the pool start to .20, checking nothing was sitting below it.

The API address needed less checking by design. .10 sits outside the DHCP pool, so the router will never hand it out, and nothing else on the network is configured to use it. Planning it that way up front means there’s nothing to conflict with when the first control plane claims it. (I’ll show how I confirmed that in the Talos install post.)

What I’d change next

The plan works, but it’s a first version:

  • Separate the lab from the household. A VLAN for the cluster would keep its traffic, and its experiments, away from everyone else’s devices. The GS308E supports VLANs, so this is mostly a configuration job.
  • Local DNS. Right now I use IP addresses everywhere. Running local DNS (something like Pi-hole) would let me use names instead.
  • A bigger static range. If I end up with more static addresses than expected, moving the pool start to around .30 gives plenty of room without disturbing anything.

None of this needs to happen before the cluster is useful. That was the point of planning first: the addresses are settled, and everything else can be layered on top without re-addressing a running control plane.

Next up: how I look after five nodes on a corner shelf without ever plugging in a monitor.