{"record":{"id":"e3bddbbc069c2a8a","repo":"grpc/grpc-go","slug":"cannot-send-secure-credentials-on-an-insecure-conn","errorCode":null,"errorMessage":"cannot send secure credentials on an insecure connection: %v","messagePattern":"cannot send secure credentials on an insecure connection: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"credentials/jwt/token_file_call_creds.go","lineNumber":79,"sourceCode":"\t\tfileReader:      &jwtFileReader{tokenFilePath: tokenFilePath},\n\t\tbackoffStrategy: backoff.DefaultExponential,\n\t}\n\n\treturn creds, nil\n}\n\n// GetRequestMetadata gets the current request metadata, refreshing tokens if\n// required. This implementation follows the PerRPCCredentials interface.  The\n// tokens will get automatically refreshed if they are about to expire or if\n// they haven't been loaded successfully yet.\n// If it's not possible to extract a token from the file, UNAVAILABLE is\n// returned.\n// If the token is extracted but invalid, then UNAUTHENTICATED is returned.\n// If errors are encoutered, a backoff is applied before retrying.\nfunc (c *jwtTokenFileCallCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {\n\tri, _ := credentials.RequestInfoFromContext(ctx)\n\tif err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot send secure credentials on an insecure connection: %v\", err)\n\t}\n\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif c.isTokenValidLocked() {\n\t\tneedsPreemptiveRefresh := time.Until(c.cachedExpiry) < preemptiveRefreshThreshold\n\t\tif needsPreemptiveRefresh && !c.pendingRefresh {\n\t\t\t// Start refresh if not pending (handling the prior RPC may have\n\t\t\t// just spawned a goroutine).\n\t\t\tc.pendingRefresh = true\n\t\t\tgo c.refreshToken()\n\t\t}\n\t\treturn map[string]string{\n\t\t\t\"authorization\": c.cachedAuthHeader,\n\t\t}, nil\n\t}\n","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/credentials/jwt/token_file_call_creds.go#L61-L97","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconn, _ := grpc.NewClient(addr,\n    grpc.WithTransportCredentials(insecure.NewCredentials()),\n    grpc.WithPerRPCCredentials(jwtCreds),\n)\n\n// after\nconn, _ := grpc.NewClient(addr,\n    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(caPool, \"\")),\n    grpc.WithPerRPCCredentials(jwtCreds),\n)","handlingStrategy":"validation","validationCode":"// Reject insecure dials whenever per-RPC bearer credentials are attached.\nif _, ok := dialOpts.security.(*insecure creds marker); isinsecure {\n    if hasPerRPCBearer(creds) { return errors.New(\"jwt creds require TLS\") }\n}\n// Simplest: always pass TLS when using jwt creds.\nconn, err := grpc.NewClient(addr,\n    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(caPool, \"\")),\n    grpc.WithPerRPCCredentials(jwtCreds),\n)","typeGuard":"func isSecureChannel(creds credentials.TransportCredentials) bool {\n    _, isInsecure := creds.(insecureMarker) // insecure package type\n    return !isInsecure\n}","tryCatchPattern":"// GetRequestMetadata runs during the RPC; surface UNAVAILABLE/UNAUTHENTICATED.\nif st, ok := status.FromError(err); ok && st.Code() == codes.Unavailable {\n    if strings.Contains(st.Message(), \"insecure connection\") {\n        // reconfigure the channel with TLS and retry\n    }\n}","preventionTips":["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."],"tags":["jwt","security","tls","transport","call-credentials","go"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}