grpc/grpc-go · error
credentials: cannot send secure credentials on an insecure…
Error message
credentials: cannot send secure credentials on an insecure connection: %v
What it means
Returned from gcpServiceAccountIdentityCallCreds.GetRequestMetadata when credentials.CheckSecurityLevel reports the connection has not reached PrivacyAndIntegrity. The credential carries a bearer JWT, so gRPC refuses to attach it to a plaintext/insecure channel. The %v holds the underlying security-level check error.
Solutions
- Dial with credentials.NewTLS(nil) or a bundle that guarantees TLS (e.g. google.NewDefaultCredentials()).
- For local testing, use a self-signed TLS transport credential instead of insecure.NewCredentials().
- Ensure RequireTransportSecurity() returning true is honored: do not override the bundle to skip transport security.
- If behind a TLS-terminating proxy, move the per-RPC credential to the hop that actually has a secure transport.
Example fix
// before
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(creds))
// after
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithPerRPCCredentials(creds)) Defensive patterns
Strategy: validation
Validate before calling
// Ensure the dial uses TLS before attaching the credential.
func secureDialOpts() grpc.DialOption {
return grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
}
// Use secureDialOpt() instead of grpc.WithTransportCredentials(insecure.NewCredentials()). Try / catch
// GetRequestMetadata errors are returned from the RPC as status; surface them:
if status.Code(err) == codes.Unauthenticated || status.Code(err) == codes.FailedPrecondition {
log.Fatal("secure credential refused insecure transport; switch the dial to TLS")
} Prevention
- Never pair these credentials with insecure.NewCredentials().
- Add a unit test asserting RequireTransportSecurity()==true is reflected by the dial.
- Use a self-signed TLS credential for local development.
When it happens
Trigger: Dialing the server with grpc.WithInsecure() or grpc.WithTransportCredentials(insecure.NewCredentials()) while using NewServiceAccountIdentityCredentials as the per-RPC credential; using a credentials.Bundle whose transport credentials negotiate a level below PrivacyAndIntegrity.
Common situations: Local development with TLS disabled for convenience; misconfigured ALTS-only bundle falling back to a plaintext transport; proxy or load balancer terminating TLS and forwarding plaintext to the backend where the credential runs.
Related errors
- cannot send secure credentials on an insecure connection
- requires SecurityLevel
- unable to transfer jwtAccess PerRPCCredentials
- unable to transfer oauthAccess PerRPCCredentials
- unable to transfer serviceAccount PerRPCCredentials
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/644c014078564dbe.
Report an issue: GitHub.
Appendix: source
Thrown at credentials/google/gcp_service_account_identity_credentials.go:130
ctx: ctx,
audience: audience,
creds: creds,
backoff: internal.BackoffStrategy,
}, nil
}
// GetRequestMetadata gets the current request metadata, refreshing tokens if
// required. This implementation follows the PerRPCCredentials interface.
//
// It guarantees that only one underlying token fetch will be executed
// concurrently. If a valid token is cached, it is returned immediately. If
// a fetch recently failed, the cached error is returned until the backoff
// interval expires. Otherwise, it initiates a new token fetch or blocks
// waiting for an already-in-progress fetch to complete.
func (c *gcpServiceAccountIdentityCallCreds) 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("credentials: cannot send secure credentials on an insecure connection: %v", err)
}
if md, err := c.cachedRequestMetadata(true); md != nil || err != nil {
return md, err
}
c.mu.Lock()
// Now that we have the lock, did someone else finish the fetch while we
// were waiting for the lock?
md, err := c.cachedRequestMetadataLocked(false)
if md != nil || err != nil {
c.mu.Unlock()
return md, err
}
// If no one is fetching, start it.
if c.fetching == nil {
c.fetching = make(chan struct{})View on GitHub (pinned to 0c51461d27)