A Kubernetes node IP is the address that the kubelet records in the Node
object as its InternalIP. Node-networking components can use that address to
identify and reach the machine. An API advertise address is the address
published as the backend of the kubernetes Service, which represents the API
server inside the cluster. A single-node cluster can store 10.41.0.1 in both
places, but they remain independent Kubernetes records with different
consumers.
Prerequisites
This note assumes Linux interface, address, and route mechanics, Kubernetes service networking, and the kubelet node-registration role.
On a laptop, the current WiFi or VPN address can disappear while Kubernetes
objects still refer to it. An empty Linux bridge named k3s-host holds
10.41.0.1/32, and k3s publishes that address as its node and API identity.
The bridge and address remain configured while external connectivity moves
between home WiFi, hotel networks, phone hotspots, and Tailscale.
Failure on a moving host
In k3s, kubernetes.default.svc is the in-cluster DNS name for the Kubernetes API server. A pod resolves that name to the kubernetes Service IP. The Service’s EndpointSlice (a Kubernetes object listing the network backends for a Service) contains the API address advertised by k3s, and kube-proxy uses that state to implement the Service.
The failure involved these concrete values:
| Object | Moving-host value | Stable value |
|---|---|---|
| Tailscale interface | 100.119.x.x | not used for cluster identity |
| Hotel WiFi interface | 192.168.202.65 | not used for cluster identity |
| k3s node/API address | whichever interface k3s selected | 10.41.0.1 |
kubernetes EndpointSlice | stale transient address | 10.41.0.1:6443 |
The laptop had working internet after changing networks, but the cluster had retained an address from an earlier network attachment. Pods sent API requests to the stable Service name; kube-proxy translated those requests to an EndpointSlice backend that no longer existed on any host interface. CoreDNS and OpenBao then timed out while trying to use the Kubernetes API.
Restarting k3s made it select the current hotel WiFi address, which restored connectivity temporarily. Moving again would repeat the failure because WiFi DHCP controls that address’s lifetime. The durable fix separates the cluster’s host-local address from external network access. WiFi and Tailscale remain responsible for reaching other machines, while the Kubernetes address remains configured on an interface whose lifetime does not follow either connection.
Create the Linux address before starting k3s
k3s can publish an address, but it does not create the Linux interface, attach the address, or install the host route. Those operations must finish first. Linux Networking explains how ip constructs rtnetlink requests and the context-dependent meaning of dev.
# Create a kernel network interface named k3s-host.
# It is a software Ethernet bridge, but it starts with no member ports.
sudo ip link add name k3s-host type bridge
# Attach the host address to k3s-host.
sudo ip address add 10.41.0.1/32 dev k3s-host
# Mark the interface administratively UP.
sudo ip link set dev k3s-host upThe Linux state produced by each command explains why k3s must start after the sequence completes:
| Command | Linux state produced | Relevance to k3s |
|---|---|---|
ip link add | k3s-host exists, with no IPv4 address and state DOWN | There is not yet an address for k3s to publish |
ip address add | 10.41.0.1/32 is configured on k3s-host; the kernel creates a local route | The host now classifies 10.41.0.1 as a local address |
ip link set dev k3s-host up | Administrative state becomes UP | k3s can start after the configured interface is enabled |
This deployment uses a bridge with no member ports, so its forwarding database has no ports between which to forward frames. The virtual-interface note compares that empty bridge with a dummy interface, the smaller alternative when a host-owned address needs no Layer-2 segment.
Publish the node and API identities
The Kubernetes API stores desired state. A connection to an ordinary
application Service does not pass through the API server: kube-proxy translates
the Service destination to one of that application’s EndpointSlice addresses,
then the CNI networking carries the packet to the workload. The
kubernetes.default.svc Service is different because its backend is the API
server itself.
After Linux has 10.41.0.1 configured locally, k3s is started with two flags:
k3s server \
--node-ip=10.41.0.1 \
--advertise-address=10.41.0.1The flags publish the same literal address through two paths. The table follows each flag to the Kubernetes state and network traffic it affects:
| Flag | Kubernetes state | Concrete consumer |
|---|---|---|
--node-ip | The kubelet reports 10.41.0.1 in Node.status.addresses as the InternalIP | Flannel normally uses internal node IPs for inter-node traffic unless configured to use external IPs; other components that read the Node address see the same stable value |
--advertise-address | The API server publishes 10.41.0.1 in the kubernetes Service EndpointSlice | Pods and components use this backend when they call kubernetes.default.svc |
An application Service has its own EndpointSlice, so the API advertise address
does not select its workload backend. If that backend is on another node,
Flannel may then use the destination node’s InternalIP to carry the packet
between hosts. For kubectl logs, exec, and port forwarding, the client first
calls the API server; in the default K3s setup, the API server reaches the
kubelet through the K3s agent tunnel rather than sending the stream directly to
the Node InternalIP.
--advertise-address is distinct from --bind-address. In k3s, the listener defaults to 0.0.0.0, while the advertise address says which reachable address other cluster members should use. Neither flag creates k3s-host, assigns 10.41.0.1, or changes a routing table.
The two explicit flags are not both required to repair the same record. The
direct fix for the stale kubernetes EndpointSlice is
--advertise-address=10.41.0.1. In current K3s, the advertise address defaults
to the configured external node IP or node IP, so on this machine
--node-ip=10.41.0.1 would normally also make the API server select that
address. Setting both avoids depending on that fallback and records two
separate decisions: keep both the Node InternalIP and the API Service backend
stable. Setting only --advertise-address would stabilize the API endpoint but
would leave kubelet’s Node IP selection tied to the available interfaces.
The host-local /32 is suitable here because this is a single-node cluster and
the destination is delivered inside the same Linux host. It is not
automatically a valid Node InternalIP for a multi-node cluster: every other
node would need a route to it, and Flannel’s inter-node transport would need a
reachable address on each node.
The published state can be checked at all three layers:
# Linux: the address is configured on the interface.
ip -brief address show dev k3s-host
# k3s-host UNKNOWN 10.41.0.1/32
# Kubernetes node registration: kubelet publishes the InternalIP.
kubectl get nodes -o wide
# INTERNAL-IP
# 10.41.0.1
# Kubernetes API service: the EndpointSlice publishes the API backend.
kubectl -n default get endpointslices kubernetes -o yaml
# endpoints:
# - addresses:
# - 10.41.0.1
# ports:
# - port: 6443These checks distinguish three failure modes: a missing Linux address, a stale Node InternalIP, and a stale API service endpoint.
Request path
The diagram follows one pod request to the special kubernetes Service, not a
generic application Service. WiFi and Tailscale addresses are absent because
the API backend is configured locally on k3s-host.
The request crosses these mechanisms:
- A pod connects to
https://kubernetes.default.svc. - Cluster DNS resolves the name to the
kubernetesService IP, such as10.43.0.1. - kube-proxy implements the Service by rewriting the destination from
10.43.0.1:443to the EndpointSlice backend10.41.0.1:6443. This is destination network address translation: the destination changes while the client continues to use the Service name and IP. - Linux route lookup recognizes
10.41.0.1as an address configured on the host. - The kernel delivers the connection to the API server listener on port
6443.
No packet needs to leave through hotel WiFi, Tailscale, or the default internet route.
Linux routing and Kubernetes service translation
Assigning the address creates the Linux local route that classifies 10.41.0.1 as host-owned. The pod packet enters through its CNI veth and bridge path. After kube-proxy rewrites the destination, Linux consults that local route and delivers the packet to the API server socket. Linux Networking owns the route fields, proto kernel, and the difference between the stored dev k3s-host and resolved dev lo outputs.
The local route and kube-proxy rule solve different parts of the request:
| Layer | Input | State consulted | Output |
|---|---|---|---|
| Kubernetes Service | destination 10.43.0.1:443 | Service and EndpointSlice | destination rewritten to 10.41.0.1:6443 |
| Linux route lookup | destination 10.41.0.1 | local routing table | packet marked for delivery inside this host |
| Socket lookup | destination port 6443 | listening TCP sockets | connection delivered to the API server process |
The setup does not add or replace a default route. WiFi DHCP, Tailscale, and NetworkManager continue to manage external connectivity independently of the host-local Kubernetes address.
Declarative NixOS configuration
The imperative commands mutate live kernel state and disappear across reboot. The deployed NixOS configuration declares the same interface and address, keeps NetworkManager from claiming the interface, and orders k3s after both network units:
let
nodeAddress = "10.41.0.1";
nodeInterface = "k3s-host";
nodeAddressUnit = "network-addresses-${nodeInterface}.service";
nodeDeviceUnit = "${nodeInterface}-netdev.service";
in
{
networking.bridges.${nodeInterface}.interfaces = [];
networking.interfaces.${nodeInterface}.ipv4.addresses = [
{ address = nodeAddress; prefixLength = 32; }
];
networking.networkmanager.unmanaged = [
"interface-name:${nodeInterface}"
];
services.k3s.extraFlags =
"--node-ip=${nodeAddress} --advertise-address=${nodeAddress}";
systemd.services.k3s = {
after = [ nodeDeviceUnit nodeAddressUnit ];
requires = [ nodeDeviceUnit nodeAddressUnit ];
};
}The requires dependency makes successful activation of both network units a requirement for k3s. The after dependency supplies startup order: the bridge and its address are configured before k3s publishes 10.41.0.1.
Sources
- K3s server CLI for
--node-ip,--advertise-address, and--bind-addresssemantics. - K3s basic network options for Flannel’s use of node internal and external addresses and K3s egress-selector tunnels.
- K3s architecture for server, agent, kubelet, CNI, and reverse-tunnel roles.
- Kubernetes Nodes and the kubelet command reference for Node registration and
--node-ipselection. - Kubernetes control-plane to node communication for API-server-to-kubelet requests such as logs, exec, and port forwarding.
- Linux networking sources for
iproute2, rtnetlink, and routes, plus virtual-interface sources for bridge and dummy behavior.
See also
- CoreDNS for one in-cluster client whose Kubernetes API watches fail when the service endpoint is unreachable.