k3s-io/k3s · error
VPN Error. Invalid control server URL for Tailscale: %w
Error message
VPN Error. Invalid control server URL for Tailscale: %w
What it means
isVPNConfigOK validates a tailscale auth config: joinKey must be non-empty, and if controlServerURL is set it must pass url.Parse. url.Parse fails only on genuinely malformed input (control characters, invalid bytes such as spaces in the host), so in practice this fires for garbled URLs - e.g. from bad templating - not merely wrong hostnames.
Source
Thrown at pkg/vpn/vpn.go:150
return vpnCliAuthInfo{}, fmt.Errorf("VPN Error. The passed VPN auth info includes an unknown parameter: %v", vpnKeyValue[0])
}
}
if err := isVPNConfigOK(authInfo); err != nil {
return authInfo, err
}
return authInfo, nil
}
// isVPNConfigOK checks that the config is complete
func isVPNConfigOK(authInfo vpnCliAuthInfo) error {
if authInfo.Name == "tailscale" {
if authInfo.JoinKey == "" {
return errors.New("VPN Error. Tailscale requires a JoinKey")
}
if authInfo.ControlServerURL != "" {
if _, err := url.Parse(authInfo.ControlServerURL); err != nil {
return fmt.Errorf("VPN Error. Invalid control server URL for Tailscale: %w", err)
}
}
return nil
}
return errors.New("Requested VPN: " + authInfo.Name + " is not supported. We currently only support tailscale")
}
// getTailscaleInfo returns the IPs of the interface
func getTailscaleInfo() (*Info, error) {
output, err := util.ExecCommand("tailscale", []string{"status", "--json"})
if err != nil {
return nil, fmt.Errorf("failed to run tailscale status --json: %v", err)
}
logrus.Debugf("Output from tailscale status --json: %v", output)
var tailscaleOutput TailscaleOutputView on GitHub (pinned to 6ba341e396)
Solutions
- Trim whitespace and newlines from the value before writing it into the auth string
- Pre-test the value: url.Parse in Go, or curl -sI <url> to confirm it is reachable
- Check connectivity to the login server separately - this validation only catches syntax, not reachability
Example fix
# before controlServerURL=https:// headscale.example.com # after controlServerURL=https://headscale.example.com
Defensive patterns
Strategy: validation
Validate before calling
for _, seg := range strings.Split(vpnAuth, ",") {
parts := strings.SplitN(seg, "=", 2)
if len(parts) == 2 && parts[0] == "controlServerURL" {
v := strings.TrimSpace(parts[1])
if _, err := url.Parse(v); err != nil {
return fmt.Errorf("invalid controlServerURL %q: %w", v, err)
}
}
} Type guard
func isParsableURL(s string) bool {
s = strings.TrimSpace(s)
_, err := url.Parse(s)
return err == nil
} Try / catch
if err := vpn.StartVPN(authFile); err != nil {
if strings.Contains(err.Error(), "Invalid control server URL") {
// strip whitespace/newlines from the URL value in the auth config and retry
}
return err
} Prevention
- Trim whitespace and newlines from URLs before embedding them in config strings
- Validate templated values with url.Parse at render time
- Remember url.Parse only catches syntax; verify login-server reachability separately
When it happens
Trigger: A controlServerURL value containing control characters, newlines, or a space inside the host portion; a value mangled by templating or copy-paste.
Common situations: Values interpolated from env vars or Helm templates with stray whitespace/newlines; copy-paste artifacts; note that a syntactically valid but wrong hostname will NOT be caught here.
Related errors
- Requested VPN: %s is not supported. We currently only suppor
- VPN Error. The passed VPN auth info includes an unknown para
- failed to run tailscale status --json: %v
- failed to unmarshal tailscale output: %v
- failed to run tailscale debug prefs: %v
AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15).
Data as JSON: /api/errors/093386b7ac5a4f5e.
Report an issue: GitHub.