grpc/grpc-go · error

cannot send secure credentials on an insecure connection: %v

Error message

cannot send secure credentials on an insecure connection: %v

What it means

Thrown from jwtTokenFileCallCreds.GetRequestMetadata (token_file_call_creds.go:79) when credentials.CheckSecurityLevel reports the connection is not at PrivacyAndIntegrity. JWT Bearer tokens are bearer secrets, so gRPC refuses to attach them to a channel that is not TLS/ALTS-protected. RequireTransportSecurity() returns true for the same reason.

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 03255a9237)

Solutions

  1. Provide real TLS transport credentials (credentials.NewTLS / credentials.NewClientTLSFromFile / NewClientTLSFromCert) as grpc.WithTransportCredentials alongside the per-RPC JWT creds.
  2. If the channel is genuinely loopback-only and you accept plaintext, drop the JWT call credentials and use local credentials instead.
  3. For ALTS environments (GCE), use alts.NewClientCreds so the security level is satisfied.

Example fix

// before
conn, _ := grpc.NewClient(addr,
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithPerRPCCredentials(jwtCreds),
)

// after
conn, _ := grpc.NewClient(addr,
    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(caPool, "")),
    grpc.WithPerRPCCredentials(jwtCreds),
)
Defensive patterns

Strategy: validation

Validate before calling

// Reject insecure dials whenever per-RPC bearer credentials are attached.
if _, ok := dialOpts.security.(*insecure creds marker); isinsecure {
    if hasPerRPCBearer(creds) { return errors.New("jwt creds require TLS") }
}
// Simplest: always pass TLS when using jwt creds.
conn, err := grpc.NewClient(addr,
    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(caPool, "")),
    grpc.WithPerRPCCredentials(jwtCreds),
)

Type guard

func isSecureChannel(creds credentials.TransportCredentials) bool {
    _, isInsecure := creds.(insecureMarker) // insecure package type
    return !isInsecure
}

Try / catch

// GetRequestMetadata runs during the RPC; surface UNAVAILABLE/UNAUTHENTICATED.
if st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {
    if strings.Contains(st.Message(), "insecure connection") {
        // reconfigure the channel with TLS and retry
    }
}

Prevention

When it happens

Trigger: Dialing with grpc.WithTransportCredentials(insecure.NewCredentials()) (or the deprecated grpc.WithInsecure()) while also attaching jwt call credentials via grpc.WithPerRPCCredentials(...). Also occurs if transport creds are omitted entirely so the default security level check fails.

Common situations: Local dev with plaintext connections to avoid cert setup; a refactor that switched dial options to insecure but left JWT creds wired in; mixing grpc.WithInsecure() with per-RPC auth in examples copied from tutorials.

Related errors


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