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, errView on GitHub (pinned to 03255a9237)
Solutions
- Provide TLS transport credentials on the same dial as the STS per-RPC credentials.
- Use alts credentials on Google infrastructure to satisfy the security-level check.
- 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
- STS-exchanged tokens are bearer secrets: TLS-only.
- Validate sts.Options at bootstrap so construction errors surface before dial.
- Use alts on Google Cloud to satisfy the security-level requirement without certs.
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
- unable to transfer TokenSource PerRPCCredentials: %v
- unable to transfer jwtAccess PerRPCCredentials: %v
- unable to transfer oauthAccess PerRPCCredentials: %v
- unable to transfer serviceAccount PerRPCCredentials: %v
- cannot send secure credentials on an insecure connection: %v
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/48dbd8e633153202.
Report an issue: GitHub.