k3s-io/k3s · error
invalid node-external-ip: %w
Error message
invalid node-external-ip: %w
What it means
The agent parses envInfo.NodeExternalIP (the --node-external-ip flag, comma-separated) with util.ParseStringSliceToIPs, which accepts bare IP addresses only; if any element fails net.ParseIP the underlying error is wrapped as 'invalid node-external-ip: ...'. CIDRs, hostnames, ranges and empty elements (trailing comma) are all invalid.
Source
Thrown at pkg/agent/config/config.go:505
if err := os.MkdirAll(nodeConfigPath, 0755); err != nil {
return nil, err
}
oldNodePasswordFile := filepath.Join(envInfo.DataDir, "agent", "node-password.txt")
newNodePasswordFile := filepath.Join(nodeConfigPath, "password")
upgradeOldNodePasswordPath(oldNodePasswordFile, newNodePasswordFile)
if controlConfig.ClusterIPRange != nil {
if utilsnet.IPFamilyOfCIDR(controlConfig.ClusterIPRange) != utilsnet.IPFamilyOf(nodeIPs[0]) && len(nodeIPs) > 1 {
firstNodeIP := nodeIPs[0]
nodeIPs[0] = nodeIPs[1]
nodeIPs[1] = firstNodeIP
}
}
nodeExternalIPs, err := util.ParseStringSliceToIPs(envInfo.NodeExternalIP.Value())
if err != nil {
return nil, fmt.Errorf("invalid node-external-ip: %w", err)
}
if envInfo.WithNodeID {
nodeID, err := ensureNodeID(filepath.Join(nodeConfigPath, "id"))
if err != nil {
return nil, err
}
nodeName += "-" + nodeID
}
os.Setenv("NODE_NAME", nodeName)
kubeconfigKubelet := filepath.Join(envInfo.DataDir, "agent", "kubelet.kubeconfig")
clientKubeProxyCert := filepath.Join(envInfo.DataDir, "agent", "client-kube-proxy.crt")
clientKubeProxyKey := filepath.Join(envInfo.DataDir, "agent", "client-kube-proxy.key")
kubeconfigKubeproxy := filepath.Join(envInfo.DataDir, "agent", "kubeproxy.kubeconfig")
clientK3sControllerCert := filepath.Join(envInfo.DataDir, "agent", "client-"+version.Program+"-controller.crt")
clientK3sControllerKey := filepath.Join(envInfo.DataDir, "agent", "client-"+version.Program+"-controller.key")View on GitHub (pinned to 6ba341e396)
Solutions
- Pass plain IPs only: --node-external-ip=203.0.113.7 (or a comma-separated list of IPs)
- Resolve hostnames to IPs yourself before passing the flag
- Strip whitespace and drop empty items from the value
Example fix
# before --node-external-ip="203.0.113.7/32," # after --node-external-ip="203.0.113.7"
Defensive patterns
Strategy: validation
Validate before calling
for _, s := range strings.Split(value, ",") {
s = strings.TrimSpace(s)
if s == "" || net.ParseIP(s) == nil {
return fmt.Errorf("--node-external-ip accepts bare IPs only, got %q", s)
}
} Type guard
func validExternalIPs(v string) bool {
for _, s := range strings.Split(v, ",") {
if net.ParseIP(strings.TrimSpace(s)) == nil {
return false
}
}
return true
} Prevention
- Pass bare IPs, never CIDRs or hostnames, to --node-external-ip
- Trim whitespace and remove trailing commas in templated flags
When it happens
Trigger: --node-external-ip=203.0.113.7/32 (CIDR), --node-external-ip=nat.example.com (hostname), '1.2.3.4,' (trailing comma), whitespace inside elements, or a malformed IPv6 literal.
Common situations: Copy/pasting a CIDR from cluster-cidr docs; templates injecting an unset variable leaving stray commas; expecting hostname resolution where none is supported.
Related errors
- --server is required
- cluster-cidr: %v and service-cidr: %v, must share the same I
- Initial server URL host is not defined for load balancer
- etcd-snapshot-reconcile-interval must be greater than 0s
- no IPv6 CIDRs found
AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15).
Data as JSON: /api/errors/d580f32feb5f5680.
Report an issue: GitHub.