hashicorp/terraform · error

no suitable TCP ports

Error message

no suitable TCP ports (between %d and %d) are available for the temporary OAuth callback server

What it means

Thrown by `LoginCommand.listenerForCallback` after exhausting `maxTries` (150% of the port range size) random attempts to `net.Listen("tcp4", "127.0.0.1:<port>")` across the `[minPort, maxPort]` window advertised by the server's OAuth client config. The temporary local HTTP server is needed to receive the OAuth authorization-code redirect. This is a local-resource exhaustion error, not a remote-server error.

Solutions

  1. Free loopback ports: stop other local servers / Docker containers / competing `terraform login` processes, then retry.
  2. Raise the OS ephemeral port range (Linux): `sysctl -w net.ipv4.ip_local_port_range="10000 65000"` so more candidates fall inside the advertised window.
  3. Allow the process through endpoint/firewall software that may be blocking loopback TCP binds.
  4. Retry on a different machine or runner where fewer ports are occupied.
  5. If controlling the TFE server, widen the advertised `min_port`/`max_port` in the OAuth client config.

Example fix

# Linux: widen ephemeral port range
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65000"
# then retry
terraform login app.terraform.io
Defensive patterns

Strategy: validation

Validate before calling

// Check that at least one port in a candidate range is bindable before login.
func freePortAvailable(min, max uint16) bool {
    for i := 0; i < 50; i++ {
        p := int(min) + rand.Intn(int(max)-int(min))
        l, err := net.Listen("tcp4", fmt.Sprintf("127.0.0.1:%d", p))
        if err == nil { l.Close(); return true }
    }
    return false
}

Type guard

null

Try / catch

// net.Listen errors are not retried inside listenerForCallback beyond maxTries;
// callers cannot retry meaningfully. Surface the error and advise freeing ports.

Prevention

When it happens

Trigger: Every randomly chosen port in the range returned a non-nil error from `net.Listen`. The range comes from `clientConfig.MinPort`/`clientConfig.MaxPort` discovered via the host's OAuth service descriptor. The loop runs `availCount + availCount/2` times before giving up.

Common situations: A development machine with many local services (Docker, other IDEs, preview servers) consuming ephemeral ports; a CI runner with a restricted or crowded port range; security software (endpoint agents) holding or blocking loopback binds; Linux with a narrow `net.ipv4.ip_local_port_range` that excludes the advertised window; another `terraform login` already in flight holding a port.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/e8bc88ab211c9c4b. Report an issue: GitHub.

Appendix: source

Thrown at internal/command/login.go:752

	// another.
	maxTries := availCount + (availCount / 2)

	for tries := 0; tries < maxTries; tries++ {
		port := rand.Intn(availCount) + int(minPort)
		addr := fmt.Sprintf("127.0.0.1:%d", port)
		log.Printf("[TRACE] login: trying %s as a listen address for temporary OAuth callback server", addr)
		l, err := net.Listen("tcp4", addr)
		if err == nil {
			// We use a path that doesn't end in a slash here because some
			// OAuth server implementations don't allow callback URLs to
			// end with slashes.
			callbackURL := fmt.Sprintf("http://localhost:%d/login", port)
			log.Printf("[TRACE] login: callback URL will be %s", callbackURL)
			return l, callbackURL, nil
		}
	}

	return nil, "", fmt.Errorf("no suitable TCP ports (between %d and %d) are available for the temporary OAuth callback server", minPort, maxPort)
}

func (c *LoginCommand) proofKey() (key, challenge string, err error) {
	// Wel use a UUID-like string as the "proof key for code exchange" (PKCE)
	// that will eventually authenticate our request to the token endpoint.
	// Standard UUIDs are explicitly not suitable as secrets according to the
	// UUID spec, but our go-uuid just generates totally random number sequences
	// formatted in the conventional UUID syntax, so that concern does not
	// apply here: this is just a 128-bit crypto-random number.
	uu, err := uuid.GenerateUUID()
	if err != nil {
		return "", "", err
	}

	key = fmt.Sprintf("%s.%09d", uu, rand.Intn(999999999))

	h := sha256.New()
	h.Write([]byte(key))

View on GitHub (pinned to d32a084675)