tailscale/tailscale · error

failed to parse base URL %q: %w

Error message

failed to parse base URL %q: %w

What it means

createClient parses the base URL for the Tailscale API client - spec.loginURL when set, otherwise ipn.DefaultControlURL. url.Parse is lenient (scheme-less strings still parse), so it errors only on genuinely malformed input such as control characters or invalid percent-escapes; when it does, the offending URL is included in the message.

Source

Thrown at k8s-operator/reconciler/tailnet/tailnet.go:260

const (
	clientIDKey     = "client_id"
	clientSecretKey = "client_secret"
	audienceKey     = "audience"
)

func (r *Reconciler) createClient(tailnet *tsapi.Tailnet, secret *corev1.Secret) (tsclient.Client, error) {
	if r.clientFunc != nil {
		return r.clientFunc(tailnet, secret), nil
	}

	baseURL := ipn.DefaultControlURL
	if tailnet.Spec.LoginURL != "" {
		baseURL = tailnet.Spec.LoginURL
	}

	base, err := url.Parse(baseURL)
	if err != nil {
		return nil, fmt.Errorf("failed to parse base URL %q: %w", baseURL, err)
	}

	var auth tailscale.Auth

	clientID := string(secret.Data[clientIDKey])
	audience := string(secret.Data[audienceKey])
	clientSecret := string(secret.Data[clientSecretKey])

	switch {
	case audience != "":
		// If the audience field is present, we assume workload identity as the authentication method.
		auth = &tailscale.IdentityFederation{
			ClientID:    clientID,
			IDTokenFunc: r.createToken(audience),
		}
	case clientSecret != "":
		// For a client secret, we assume oauth.
		auth = &tailscale.OAuth{

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Reveal invisible characters: kubectl get tailnet <name> -o jsonpath='{.spec.loginURL}' | xxd | head
  2. Normalize to a clean https URL and re-apply the Tailnet
  3. Drop the field entirely to use the default control plane
  4. Validate the field at admission with a CEL rule or webhook

Example fix

# before
spec:
  loginURL: "https://ctrl.example.com\n"   # trailing newline breaks url.Parse
# after
spec:
  loginURL: "https://ctrl.example.com"
Defensive patterns

Strategy: validation

Validate before calling

func validLoginURL(s string) bool {
    if s == "" {
        return true // default control URL
    }
    u, err := url.Parse(s)
    return err == nil && u.Scheme != "" && u.Host != ""
}

Prevention

When it happens

Trigger: url.Parse(baseURL) failing: spec.loginURL containing control characters (newline, NUL), invalid escapes like %zz, or other characters Go's URL parser rejects. The default control URL is a constant and always valid.

Common situations: Copy-pasted control server URLs with trailing newlines; Helm/templating injecting stray characters; values round-tripped through ConfigMaps without trimming.

Understand the failure class

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/8f60f9fb51ee7171. Report an issue: GitHub.