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
- Send basic auth with the expected password: req.SetBasicAuth("user", password) (username is ignored, only the password is compared).
- On macOS, use the official client libraries/CLI which handle the auth handshake.
- 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
- Obtain the password through the platform helper that issued it; never hard-code.
- One shared HTTP client with auth set covers all endpoints.
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.