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
- Provide real TLS transport credentials (credentials.NewTLS / credentials.NewClientTLSFromFile / NewClientTLSFromCert) as grpc.WithTransportCredentials alongside the per-RPC JWT creds.
- If the channel is genuinely loopback-only and you accept plaintext, drop the JWT call credentials and use local credentials instead.
- 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
- Never pair grpc.WithTransportCredentials(insecure.NewCredentials()) with per-RPC bearer credentials.
- Add an integration test that asserts RequireTransportSecurity()==true creds are only used on TLS channels.
- In local dev, use a self-signed CA via NewClientTLSFromCert instead of insecure.
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
- unable to transfer jwtAccess PerRPCCredentials: %v
- unable to transfer TokenSource PerRPCCredentials: %v
- unable to transfer oauthAccess PerRPCCredentials: %v
- unable to transfer serviceAccount PerRPCCredentials: %v
- unable to transfer STS PerRPCCredentials: %v
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/e3bddbbc069c2a8a.
Report an issue: GitHub.