grpc/grpc-go · error
unable to transfer serviceAccount PerRPCCredentials: %v
Error message
unable to transfer serviceAccount PerRPCCredentials: %v
What it means
Thrown from serviceAccount.GetRequestMetadata (oauth.go:171) when CheckSecurityLevel fails for credentials built via oauth.NewServiceAccountFromKey/NewServiceAccountFromFile. The service-account flow mints an OAuth2 access token, which is a bearer secret, so it is rejected on a connection below PrivacyAndIntegrity. RequireTransportSecurity() returns true.
Source
Thrown at credentials/oauth/oauth.go:171
type serviceAccount struct {
mu sync.Mutex
config *jwt.Config
t *oauth2.Token
}
func (s *serviceAccount) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.t.Valid() {
var err error
s.t, err = s.config.TokenSource(ctx).Token()
if err != nil {
return nil, err
}
}
ri, _ := credentials.RequestInfoFromContext(ctx)
if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, fmt.Errorf("unable to transfer serviceAccount PerRPCCredentials: %v", err)
}
return map[string]string{
"authorization": s.t.Type() + " " + s.t.AccessToken,
}, nil
}
func (s *serviceAccount) RequireTransportSecurity() bool {
return true
}
// NewServiceAccountFromKey constructs the PerRPCCredentials using the JSON key slice
// from a Google Developers service account.
func NewServiceAccountFromKey(jsonKey []byte, scope ...string) (credentials.PerRPCCredentials, error) {
config, err := google.JWTConfigFromJSON(jsonKey, scope...)
if err != nil {
return nil, err
}
return &serviceAccount{config: config}, nilView on GitHub (pinned to 03255a9237)
Solutions
- Pair the per-RPC service-account creds with TLS transport credentials.
- On Google Cloud, use alts credentials or ComputeTokenSource over ALTS so the security level is satisfied.
- Remove the per-RPC credentials if the channel is intentionally plaintext.
Example fix
// before
creds, _ := oauth.NewServiceAccountFromFile(keyPath, scope)
conn, _ := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithPerRPCCredentials(creds),
)
// after
creds, _ := oauth.NewServiceAccountFromFile(keyPath, scope)
conn, _ := grpc.NewClient(addr,
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(caPool, "")),
grpc.WithPerRPCCredentials(creds),
) Defensive patterns
Strategy: validation
Validate before calling
saCreds, err := oauth.NewServiceAccountFromFile(keyPath, scope)
if err != nil { return err }
tlsCreds := credentials.NewClientTLSFromCert(caPool, "")
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(tlsCreds),
grpc.WithPerRPCCredentials(saCreds),
) Try / catch
if st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {
if strings.Contains(st.Message(), "serviceAccount PerRPCCredentials") {
// channel is insecure; re-dial with TLS
}
} Prevention
- Pair service-account per-RPC creds with TLS on every dial.
- On GCE/GKE prefer alts or ComputeTokenSource to avoid this class entirely.
- Static analysis: flag dials that pair serviceAccount creds with insecure transport.
When it happens
Trigger: Dialing with insecure.NewCredentials() (or no transport creds) while attaching service-account per-RPC credentials.
Common situations: Service-account auth configured for a dev environment that runs plaintext; refactored dial options that dropped TLS but kept per-RPC creds; copying tutorial code that omitted TLS.
Related errors
- unable to transfer TokenSource PerRPCCredentials: %v
- unable to transfer jwtAccess PerRPCCredentials: %v
- unable to transfer oauthAccess PerRPCCredentials: %v
- unable to transfer STS 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/a332d2f582f50554.
Report an issue: GitHub.