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 TailscaleOutput

View on GitHub (pinned to 6ba341e396)

Solutions

  1. Trim whitespace and newlines from the value before writing it into the auth string
  2. Pre-test the value: url.Parse in Go, or curl -sI <url> to confirm it is reachable
  3. 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

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


AI-assisted analysis of k3s-io/k3s@6ba341e396 (2026-08-15). Data as JSON: /api/errors/093386b7ac5a4f5e. Report an issue: GitHub.