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}, nil

View on GitHub (pinned to 03255a9237)

Solutions

  1. Pair the per-RPC service-account creds with TLS transport credentials.
  2. On Google Cloud, use alts credentials or ComputeTokenSource over ALTS so the security level is satisfied.
  3. 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

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


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