Network¶
Part of the implementation guide
Run this phase with Phase 1 — Network and
playbook-set-static-ip.yml.
L2 Bridge to the Physical LAN¶
The 3 nodes use a Linux bridge (br0) over the physical interface,
so that Incus instances receive IP directly from the LAN (local network) and are
reachable between hosts without needing OVN (Open Virtual Network).
| Node | Physical Interface | IP on br0 (static) |
|---|---|---|
| invincible | eno2 |
192.168.20.6 |
| oliver | eno1 |
192.168.20.7 |
| deborah | enP4p65s0 |
192.168.20.5 |
Confirm with
ip -br linkon the node before applying — the name of the physical interface may differ from this table after an OS update.
Example netplan — invincible:
# /etc/netplan/br0.yaml
network:
version: 2
renderer: networkd
ethernets:
eno2:
dhcp4: no
bridges:
br0:
interfaces: [eno2]
addresses: [192.168.20.6/22]
routes:
- to: default
via: 192.168.20.1
nameservers:
addresses: [192.168.20.5, 192.168.20.1]
Example netplan — deborah (same subnet, different interface and last octet):
# /etc/netplan/br0.yaml
network:
version: 2
renderer: networkd
ethernets:
enP4p65s0:
dhcp4: no
bridges:
br0:
interfaces: [enP4p65s0]
addresses: [192.168.20.5/22]
routes:
- to: default
via: 192.168.20.1
nameservers:
addresses: [192.168.20.5, 192.168.20.1]
ip -br link
# Debian no lo trae instalado por defecto
sudo apt install -y netplan.io
# corta SSH si cambias de IP
sudo netplan apply
The Ansible playbook installs netplan.io, enables systemd-networkd and migrates
the NIC (network interface card) out of ifupdown before writing /etc/netplan/br0.yaml.
Incus profile to use the bridge in the instances:
Fixed IP — why it is mandatory here¶
- Incus cluster:
cluster.https_addressis recorded by IP when joining. - K3s (lightweight Kubernetes distribution): kubelet uses the node IP as identity.
- CAPN (Cluster API Provider for Incus): the
Secretlxc-secretpoints tohttps://<IP>:8443as a fixed string.
IP Scheme¶
| Node | Current IP (DHCP/pre-bridge) | IP on br0 |
|---|---|---|
| invincible | 192.168.23.162 | 192.168.20.6 |
| oliver | 192.168.20.87 | 192.168.20.7 |
| deborah | 192.168.21.211 | 192.168.20.5 |
The LAN is a
/22(192.168.20.0–192.168.23.255), not a/24— the "current" IPs in this table are DHCP leases within that same range (not separate subnets). The source of truth at all times isansible_hostininventory.ini.
LAN Addressing (192.168.20.0/22, gateway 192.168.20.1)¶
The router's DHCP assigns 192.168.20.31 to 192.168.23.191. Everything static lives outside that range:
| Range | Use | Where Configured |
|---|---|---|
192.168.20.1 |
Gateway (router) | Router |
192.168.20.4 |
K3s API VIP (kube-vip, optional) | kube_vip.address in k3s_install.yml |
192.168.20.5, .6, .7 |
deborah, invincible, oliver | static_ip in inventory.ini |
192.168.20.31 – 192.168.23.191 |
Router DHCP | Router |
192.168.23.192 – .199 |
Free | — |
192.168.23.200 – .220 |
MetalLB (LoadBalancer, Gateway; the Kong Gateway uses .200) |
gitops/metallb/ipaddresspool.yaml |
192.168.23.221 – .223 |
Free | — |
192.168.23.224/28 (.224–.239) |
Incus LB (OVN), uplink ipv4.routes |
incus_ovn_lb_routes in group_vars/incus_cluster/ovn.yml |
192.168.23.240 – .249 |
External OVN, ipv4.ovn.ranges (a virtual router per OVN network) |
incus_ovn_external_ranges in the same file |
192.168.23.250 – .255 |
Free (.255 is the broadcast of the /22) |
— |
Outside the LAN, private networks that do not overlap with it or each other:
| Network | Use |
|---|---|
10.42.0.0/16 and 10.43.0.0/16 |
K3s Pods and Services |
192.168.19.0/24 |
Internal OVN network (ovn-lan, with NAT) |
Outside the DHCP Range
Fixed IPs, the MetalLB pool, and OVN segments must remain outside the DHCP range
(192.168.20.31–192.168.23.191). If not, the router may assign them to another device, causing conflicts. The preflight check for the
role incus_ovn fails if its segments overlap with DHCP or MetalLB.
After playbook-set-static-ip.yml, update ansible_host in
inventory.ini
to the value of static_ip if the connection IP changed.
This decision in other environments
MetalLB takes free IPs from the LAN (.200–.220) for K3s Services LoadBalancer, and all web traffic enters through a single Kong Gateway (Gateway API), with
Calico as CNI. No need to buy a load balancer: just reserve a range outside the DHCP.
- Benefits: MetalLB provides IPs of
LoadBalanceron your own servers without extra hardware, and Gateway API is a standard: routes (HTTPRoute) are written the same even if you switch Kong for another controller. - Dedicated load balancers: many data centers already have hardware or virtual load balancers, such as F5 BIG-IP or NetScaler, and the network team prefers the cluster to use them instead of announcing IPs on its own.
- BGP instead of L2: MetalLB in BGP mode announces IPs to data center routers and distributes traffic among nodes; the L2 mode of the HomeLab sends all traffic through one node at a time.
- Commercial support: there are versions with support for the same components, such as Kong Konnect for the gateway and Calico Enterprise for networking and its policies.
- When it still makes sense: on bare metal servers, branches, or disconnected sites, where there is no cloud load balancer and MetalLB plus a gateway inside the cluster is the simplest to operate.
| Cloud | Equivalent Service |
|---|---|
| GCP | Cloud Load Balancing with the GKE Gateway controller |
| AWS | Elastic Load Balancing with the AWS Load Balancer Controller (supports Gateway API) |
| Azure | Application Gateway for Containers (Gateway API) and Azure Load Balancer for LoadBalancer |
| Oracle Cloud (OCI) | OCI Load Balancer with the OCI Native Ingress Controller (uses Ingress, not Gateway API) |
In the cloud, you do not need MetalLB: each Service LoadBalancer or Gateway creates a managed load balancer, with high availability and integrated certificates. In return, each load balancer is billed separately and its configuration uses annotations or CRDs specific to each cloud; in OCI, if you want Gateway API, you install your own controller (like Kong) behind the load balancer.
Ollama: router and nodes¶
The AI assistant has one single entry point: the router ollama-router (namespace ai). Cluster clients use the Service http://ollama-router.ai.svc; external clients, https://ollama.homelab.local (Kong, only HTTPS; the name is in the homelab.local zone of BIND). The router requires a Bearer token.
Each Ollama node (deborah 192.168.20.5, invincible 192.168.20.6, oliver 192.168.20.7, and when powered on, the Mac) has a socket on port 11434 that wakes up Ollama; internally, Ollama listens only on 127.0.0.1:11435. The socket listens only on the node's IP (variable ollama_ondemand_listen) and the node's firewall allows the port from K3s nodes and from pods (the router can be on the same node). The Ollama node does not authenticate: inference protection is the router's token plus NetworkPolicy in namespace ai (which are activated in stages). Details in
Local AI Assistant and Add a Node to the Ollama Cluster.
K3s API Names¶
Agents, kubeconfig, and certificates use the name k3s.homelab.local for the API (port 6443) instead of a specific node's IP. This way, the control plane can move without affecting clients
(procedure).
Exposed Ports and Host Firewall¶
Each node has a host firewall with nftables (role host_firewall): its own table, evaluated before
kube-proxy and not modifying Calico chains. The policy is accept with explicit denials (to avoid breaking K3s, Calico, Longhorn, or MetalLB if they change ports), and traffic between nodes is trusted. The following is restricted:
- SSH (
22) from the LAN, with authentication via Dex. - The K3s API (
6443), kubelet (10250), BGP traffic (179), FRR (2623), and MetalLB (7946) traffic, and Typha (5473): only between nodes and the pod range. - Metrics (
9100,9120,9140,9141): only to the collector and nodes. - Incus (
8443): nodes, pods, and operator. Ollama (11434): K3s nodes, pods, and machines added tohost_firewall_ollama_extra_cidrs(the Mac). - DNS (
53) from the LAN.
The list of what remains without its own authentication, with the reason and the network control that replaces it, is in Authentication Model. The rules are applied with a safety auto-reversal (see Post-Incident Hardening).