grpc/grpc-go · error

cannot send secure credentials on an insecure connection

Error message

cannot send secure credentials on an insecure connection: %v

What it means

Returned by jwtTokenFileCallCreds.GetRequestMetadata when CheckSecurityLevel finds the connection is below PrivacyAndIntegrity. The JWT bearer token is sensitive, so gRPC will not attach it to an insecure transport. The %v holds the security-level check error. Semantically identical to error 201 but emitted by the JWT-from-file credential.

Solutions

  1. Dial with credentials.NewTLS(&tls.Config{}) (or equivalent secure transport).
  2. Use a self-signed cert for local testing rather than insecure.NewCredentials().
  3. If TLS is terminated upstream, ensure the per-RPC credential runs on the secure hop or use mTLS through the proxy.

Example fix

// before
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(jwtCreds))
// after
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithPerRPCCredentials(jwtCreds))
Defensive patterns

Strategy: validation

Validate before calling

// Always dial with TLS when using JWT-from-file credentials.
conn, err := grpc.Dial(addr,
    grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ServerName: addr})),
    grpc.WithPerRPCCredentials(jwtCreds),
)

Try / catch

if status.Code(err) == codes.Unauthenticated && strings.Contains(err.Error(), "insecure connection") {
    log.Fatal("JWT credential requires TLS; do not use insecure.NewCredentials()")
}

Prevention

When it happens

Trigger: Dialing with insecure.NewCredentials() while registering a jwt.NewTokenFileCallCredentials per-RPC credential; a bundle that downgrades the transport; TLS termination upstream leaving plaintext on this hop.

Common situations: Local dev without TLS; sidecar/mesh stripping TLS; misconfigured credentials bundle where RequireTransportSecurity()==true is contradicted by the transport.

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/e3bddbbc069c2a8a. Report an issue: GitHub.

Appendix: source

Thrown at credentials/jwt/token_file_call_creds.go:79

		fileReader:      &jwtFileReader{tokenFilePath: tokenFilePath},
		backoffStrategy: backoff.DefaultExponential,
	}

	return creds, nil
}

// GetRequestMetadata gets the current request metadata, refreshing tokens if
// required. This implementation follows the PerRPCCredentials interface.  The
// tokens will get automatically refreshed if they are about to expire or if
// they haven't been loaded successfully yet.
// If it's not possible to extract a token from the file, UNAVAILABLE is
// returned.
// If the token is extracted but invalid, then UNAUTHENTICATED is returned.
// If errors are encoutered, a backoff is applied before retrying.
func (c *jwtTokenFileCallCreds) 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("cannot send secure credentials on an insecure connection: %v", err)
	}

	c.mu.Lock()
	defer c.mu.Unlock()

	if c.isTokenValidLocked() {
		needsPreemptiveRefresh := time.Until(c.cachedExpiry) < preemptiveRefreshThreshold
		if needsPreemptiveRefresh && !c.pendingRefresh {
			// Start refresh if not pending (handling the prior RPC may have
			// just spawned a goroutine).
			c.pendingRefresh = true
			go c.refreshToken()
		}
		return map[string]string{
			"authorization": c.cachedAuthHeader,
		}, nil
	}

View on GitHub (pinned to 0c51461d27)