gravitational/teleport · error

max retry attempts reached: %w

Error message

max retry attempts reached: %w

What it means

The FIDO2 device callback wrapper retries transient failures (e.g. touch required, wrong PIN attempts) up to a bounded number of attempts. When the retry budget is exhausted while the last attempt still returned an error, it returns "max retry attempts reached" wrapping the most recent error. This prevents infinite retry loops against misbehaving or repeatedly-failing devices.

Source

Thrown at lib/auth/webauthncli/fido2.go:1025

			// See https://github.com/Yubico/libfido2/blob/main/src/fido/err.h#L32.
			switch fidoErr.Code {
			case 60: // FIDO_ERR_UV_BLOCKED, 0x3c
				const msg = "" +
					"The user verification function in your security key is blocked. " +
					"This is likely due to too many failed authentication attempts. " +
					"Consult your manufacturer documentation for how to unblock your security key. " +
					"Alternatively, you may unblock your device by using it in the Web UI."
				return trace.Wrap(err, msg)
			case 63: // FIDO_ERR_UV_INVALID, 0x3f
				fidoLog.DebugContext(context.Background(), "Retrying libfido2 error 63")
				continue
			default: // Unexpected code.
				return err
			}
		}

		return fmt.Errorf("max retry attempts reached: %w", err)
	}
}

func withPINHandler(cb deviceCallbackFunc) pinAwareCallbackFunc {
	return func(dev FIDODevice, info *deviceInfo, pin string) (requiresPIN bool, err error) {
		// Attempt to select a device by running "deviceCallback" on it.
		// For most scenarios this works, saving a touch.
		err = cb(dev, info, pin)
		switch {
		case errors.Is(err, libfido2.ErrPinRequired):
			// Continued below.
		case errors.Is(err, libfido2.ErrUnsupportedOption) && pin == "" && !info.uv && info.clientPinSet:
			// The failing option is likely to be "UV", so we handle this the same as
			// ErrPinRequired: see if the user selects this device, ask for the PIN and
			// try again.
			// Continued below.
		default:
			return

View on GitHub (pinned to 1283425b60)

Solutions

  1. Wait for the UV-blocked/cooldown period to expire and retry with the correct PIN.
  2. Replace retry loops with user interaction: prompt the user to touch the key / re-enter PIN between attempts.
  3. Unplug and reinsert the security key (or reset it if necessary) to clear a stuck state.
  4. Increase the max retry count in the retry wrapper only if the operation is known to need more attempts, and add backoff between retries.

Example fix

// before
for i := 0; i < maxRetries; i++ {
    return deviceCallback(dev) // fails repeatedly, no backoff
}
// after
for i := 0; i < maxRetries; i++ {
    err := deviceCallback(dev)
    if err == nil { return nil }
    time.Sleep(backoff(i)) // give the device/user time to recover
}
Defensive patterns

Strategy: retry

Validate before calling

info, err := dev.Info()
if err != nil { return err }
if info.AuthenticatorConfig != nil && uvBlocked(info) {
    return errors.New("device UV is temporarily blocked; wait before retrying")
}

Type guard

func isRetryableFIDO2Error(err error) bool {
    var le *libfido2.Error
    if errors.As(err, &le) {
        switch le.Code {
        case libfido2.ErrTouchRequired, libfido2.ErrPinInvalid, libfido2.ErrPinAuthBlocked:
            return le.Code != libfido2.ErrPinAuthBlocked // blocked needs cooldown, not blind retry
        }
    }
    return false
}

Try / catch

assertion, err := fido2Login(ctx, cfg, user, prompt)
if err != nil {
    if strings.Contains(err.Error(), "max retry attempts reached") {
        return trace.Wrap(err, "ask the user to touch the key or re-enter the correct PIN, then retry")
    }
    return trace.Wrap(err)
}

Prevention

When it happens

Trigger: A fido2 device operation (login/register callback) fails repeatedly with a retryable status (e.g. FIDO_ERR_UV_BLOCKED, wrong PIN, device busy) for the maximum number of attempts, so the loop exits via the final return.

Common situations: User entering the wrong FIDO2 PIN repeatedly until UV is blocked; a security key left in a busy/unresponsive state; device drivers or libfido2 returning transient errors continuously; automated scripts hammering a device without human touch.

Related errors


AI-assisted analysis of gravitational/teleport@1283425b60 (2026-09-02). Data as JSON: /api/errors/68a6aa7eb9335b32. Report an issue: GitHub.