tailscale/tailscale · error

auth required

Error message

auth required

What it means

Returned (HTTP 401 Unauthorized) when the localapi Handler has RequiredPassword set but the request carries no HTTP basic-auth credentials. RequiredPassword is used by the sandboxed macOS GUI auth mechanism (sameuserproof) and test harnesses; in that mode every request must include basic auth. The WWW-Authenticate context is that BasicAuth() parsing found no Authorization header or a non-basic scheme.

Source

Thrown at ipn/localapi/localapi.go:263

	if h.b == nil {
		http.Error(w, "server has no local backend", http.StatusInternalServerError)
		return
	}
	if r.Referer() != "" || r.Header.Get("Origin") != "" || !h.validHost(r.Host) {
		metricInvalidRequests.Add(1)
		http.Error(w, "invalid localapi request", http.StatusForbidden)
		return
	}
	w.Header().Set("Tailscale-Version", version.Long())
	w.Header().Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion)))
	w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
	w.Header().Set("X-Frame-Options", "DENY")
	w.Header().Set("X-Content-Type-Options", "nosniff")
	if h.RequiredPassword != "" {
		_, pass, ok := r.BasicAuth()
		if !ok {
			metricInvalidRequests.Add(1)
			http.Error(w, "auth required", http.StatusUnauthorized)
			return
		}
		if subtle.ConstantTimeCompare([]byte(pass), []byte(h.RequiredPassword)) == 0 {
			metricInvalidRequests.Add(1)
			http.Error(w, "bad password", http.StatusForbidden)
			return
		}
	}
	defer h.b.CheckDeadlocks()()
	if fn, route, ok := handlerForPath(r.URL.Path); ok {
		h.logRequest(r.Method, route)
		fn(h, w, r)
	} else {
		http.NotFound(w, r)
	}
}

// validLocalHostForTesting allows loopback handlers without RequiredPassword for testing.

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Send basic auth with the expected password: req.SetBasicAuth("user", password) (username is ignored, only the password is compared).
  2. On macOS, use the official client libraries/CLI which handle the auth handshake.
  3. If you control the server side and don't need it, leave RequiredPassword empty for socket-based auth.

Example fix

// before
req, _ := http.NewRequest("GET", "http://local-tailscaled.sock/localapi/v0/status", nil)
// after
req, _ := http.NewRequest("GET", "http://local-tailscaled.sock/localapi/v0/status", nil)
req.SetBasicAuth("tailscale", localAPIPassword)
Defensive patterns

Strategy: validation

Validate before calling

if serverRequiresPassword { // macOS sameuserproof mode
	req.SetBasicAuth("tailscale", password) // only the password is checked
}

Try / catch

if resp.StatusCode == http.StatusUnauthorized {
	return errors.New("localapi password required: add basic auth")
}

Prevention

When it happens

Trigger: Sending any request to the localapi socket without an Authorization: Basic ... header while RequiredPassword is non-empty — typical on macOS where the GUI connects through the sameuserproof socket.

Common situations: Custom scripts hitting the macOS tailscaled socket without the password; running localapi in test mode with a password and forgetting credentials in one client.

Related errors


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