grpc/grpc-go · error

unable to transfer STS PerRPCCredentials: %v

Error message

unable to transfer STS PerRPCCredentials: %v

What it means

Thrown from callCreds.GetRequestMetadata in sts/sts.go:155 when CheckSecurityLevel fails. STS-exchanged tokens are bearer credentials, so gRPC refuses to attach them on a connection below PrivacyAndIntegrity. RequireTransportSecurity() returns true, and the STS HTTP exchange itself also uses TLS to the token endpoint.

Source

Thrown at credentials/sts/sts.go:155

// callCreds provides the implementation of call credentials based on an STS
// token exchange.
type callCreds struct {
	opts   Options
	client httpDoer

	// Cached accessToken to avoid an STS token exchange for every call to
	// GetRequestMetadata.
	mu            sync.Mutex
	tokenMetadata map[string]string
	tokenExpiry   time.Time
}

// GetRequestMetadata returns the cached accessToken, if available and valid, or
// fetches a new one by performing an STS token exchange.
func (c *callCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
	ri, _ := credentials.RequestInfoFromContext(ctx)
	if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
		return nil, fmt.Errorf("unable to transfer STS PerRPCCredentials: %v", err)
	}

	// Holding the lock for the whole duration of the STS request and response
	// processing ensures that concurrent RPCs don't end up in multiple
	// requests being made.
	c.mu.Lock()
	defer c.mu.Unlock()

	if md := c.cachedMetadata(); md != nil {
		return md, nil
	}
	req, err := constructRequest(ctx, c.opts)
	if err != nil {
		return nil, err
	}
	respBody, err := sendRequest(c.client, req)
	if err != nil {
		return nil, err

View on GitHub (pinned to 03255a9237)

Solutions

  1. Provide TLS transport credentials on the same dial as the STS per-RPC credentials.
  2. Use alts credentials on Google infrastructure to satisfy the security-level check.
  3. Remove the STS per-RPC credentials if the channel is intentionally plaintext.

Example fix

// before
stsCreds, _ := sts.NewCredentials(opts)
conn, _ := grpc.NewClient(addr,
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithPerRPCCredentials(stsCreds),
)

// after
stsCreds, _ := sts.NewCredentials(opts)
conn, _ := grpc.NewClient(addr,
    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(caPool, "")),
    grpc.WithPerRPCCredentials(stsCreds),
)
Defensive patterns

Strategy: validation

Validate before calling

stsCreds, err := sts.NewCredentials(opts)
if err != nil { return err }
tlsCreds := credentials.NewClientTLSFromCert(caPool, "")
conn, err := grpc.NewClient(addr,
    grpc.WithTransportCredentials(tlsCreds),
    grpc.WithPerRPCCredentials(stsCreds),
)

Try / catch

if st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {
    if strings.Contains(st.Message(), "STS PerRPCCredentials") {
        // channel is insecure; re-dial with TLS
    }
}

Prevention

When it happens

Trigger: Using sts.NewCredentials(opts) as per-RPC credentials on a channel created with insecure.NewCredentials() or with no transport credentials.

Common situations: Workload-identity / token-broker setups wired onto a plaintext dev channel; dropping TLS during debugging; xDS bootstrap pointing STS at the gRPC channel but the channel itself is insecure.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/48dbd8e633153202. Report an issue: GitHub.