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

  1. Dial with credentials.NewTLS(nil) or a bundle that guarantees TLS (e.g. google.NewDefaultCredentials()).
  2. For local testing, use a self-signed TLS transport credential instead of insecure.NewCredentials().
  3. Ensure RequireTransportSecurity() returning true is honored: do not override the bundle to skip transport security.
  4. 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

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


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)