Skip to content

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 link on 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.

ansible-playbook -i inventory.ini playbook-set-static-ip.yml --limit deborah

Incus profile to use the bridge in the instances:

incus profile device add default eth0 nic nictype=bridged parent=br0

Fixed IP — why it is mandatory here

  1. Incus cluster: cluster.https_address is recorded by IP when joining.
  2. K3s (lightweight Kubernetes distribution): kubelet uses the node IP as identity.
  3. CAPN (Cluster API Provider for Incus): the Secret lxc-secret points to https://<IP>:8443 as 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 is ansible_host in inventory.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 LoadBalancer on 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 to host_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).