tailscale/tailscale · error

received empty authkey from control server

Error message

received empty authkey from control server

What it means

The identity-federation authkey wrapper called the Tailscale control server's key-creation API and got a successful response, but the returned key string was empty. The code refuses to hand back an empty credential and fails with this error. An empty key with a 2xx response points at the control plane (or something between) rather than at the caller's inputs.

Source

Thrown at feature/identityfederation/identityfederation.go:89

	tsClient := tailscale.NewClient("-", tailscale.APIKey(accessToken))
	tsClient.UserAgent = "tailscale-cli-identity-federation"
	tsClient.BaseURL = args.BaseURL

	authkey, _, err := tsClient.CreateKey(ctx, tailscale.KeyCapabilities{
		Devices: tailscale.KeyDeviceCapabilities{
			Create: tailscale.KeyDeviceCreateCapabilities{
				Reusable:      false,
				Ephemeral:     ephemeral,
				Preauthorized: preauth,
				Tags:          args.Tags,
			},
		},
	})
	if err != nil {
		return "", fmt.Errorf("unexpected error while creating authkey: %w", err)
	}
	if authkey == "" {
		return "", errors.New("received empty authkey from control server")
	}

	return authkey, nil
}

func parseOptionalAttributes(clientID string) (strippedID string, ephemeral bool, preauthorized bool, err error) {
	strippedID, attrs, found := strings.Cut(clientID, "?")
	if !found {
		return clientID, true, false, nil
	}

	parsed, err := url.ParseQuery(attrs)
	if err != nil {
		return "", false, false, fmt.Errorf("failed to parse optional config attributes: %w", err)
	}

	for k := range parsed {
		switch k {

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Inspect the raw key-creation HTTP response; the client expects a non-empty key string (e.g. tskey-auth-...) in the body
  2. If you run the control server or a mock, make it return the key field in the create-key response
  3. Verify the control server's API version matches what this client expects
  4. Retry with backoff once or twice; a transient control-plane glitch can return 2xx with an empty body

Example fix

// before (mock control server)
func handleCreateKey(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(200)
	io.WriteString(w, `{"usable":true}`)
}
// after
func handleCreateKey(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(200)
	io.WriteString(w, `{"key":"tskey-auth-k1234567890abcdef","usable":true}`)
}
Defensive patterns

Strategy: retry

Try / catch

key, err := createAuthKey(ctx, args)
if err != nil {
	if strings.Contains(err.Error(), "empty authkey") {
		// transient control-server behavior; retry with backoff, then fail loudly
		key, err = retryWithBackoff(ctx, 3, func() (string, error) { return createAuthKey(ctx, args) })
	}
	if err != nil {
		return "", fmt.Errorf("authkey creation failed: %w", err)
	}
}

Prevention

When it happens

Trigger: Calling the wrapper around tailscale Keys().Create (Reusable=false, Ephemeral/Preauthorized parsed from the client-ID query attributes, Tags passed through) when the server responds 2xx with a missing/empty key field: a stub or headtest control server that omits the key, an API version that renamed the field, or a proxy that strips the response body.

Common situations: Running integration tests against a mock control server that returns {} or {"usable":true} with no key; API schema drift after a control-plane upgrade; a throttling layer returning 200 with an empty body.

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/9ed3ba7bfaece8ff. Report an issue: GitHub.