gRPC has become the standard internal API protocol for microservices architectures. It’s fast, strongly typed, code-generates client and server stubs from .proto definitions, and handles streaming cleanly. Most teams adopt it by following the getting-started documentation, standing up a server in a couple of hours, and moving on to building features. The security model, which requires explicit decisions about TLS, authentication, and service exposure, often gets deferred.
That deferral creates a consistent set of vulnerabilities across gRPC deployments. They’re not exotic flaws — they’re the result of default configurations that are insecure combined with documentation that doesn’t always make the security implications obvious. This guide covers the four most commonly exploited.
Vulnerability 1: Missing TLS (Plaintext gRPC)
gRPC defaults to plaintext (insecure) connections. The getting-started examples in the official Go documentation explicitly use grpc.NewServer() without TLS credentials, and note that this is for “testing purposes.” In practice, a significant proportion of internal gRPC services run without TLS, on the reasoning that “it’s internal anyway.”
The risks of plaintext gRPC in production:
- Credentials, tokens, and sensitive data in request payloads are transmitted in cleartext
- Any attacker or compromised system on the internal network can intercept and read all gRPC traffic
- Service identity cannot be verified without mutual TLS — MITM attacks are possible
- Protocol buffers serialise to binary but are not encrypted; tooling like
grpc-dumptrivially captures and decodes traffic
Go: Insecure (don’t do this in production)
server := grpc.NewServer()
// No TLS — traffic is cleartext
Go: Secure with TLS
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
log.Fatalf("failed to load TLS credentials: %v", err)
}
server := grpc.NewServer(grpc.Creds(creds))
Python: Secure with TLS
with open('server.key', 'rb') as f:
private_key = f.read()
with open('server.crt', 'rb') as f:
certificate_chain = f.read()
server_credentials = grpc.ssl_server_credentials(
[(private_key, certificate_chain)]
)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
server.add_secure_port('[::]:50051', server_credentials)
For internal service-to-service communication, mutual TLS (mTLS) provides both encryption and service identity verification. Service meshes like Istio and Linkerd can handle mTLS transparently without requiring application-level certificate management.
Vulnerability 2: Exposed gRPC Reflection API
gRPC Server Reflection is a service that lets clients query a running gRPC server to discover what services and methods it exposes — including the full protobuf schema. This is extremely useful for debugging with tools like grpc_cli or grpcui. It’s also a significant information disclosure vulnerability if left enabled in production.
An attacker (or an internal system that should not have broad access) with network access to a gRPC server with reflection enabled can:
- Enumerate all services and methods available on the server
- Retrieve the complete protobuf schema definitions, including field names and types
- Discover internal service capabilities that weren’t intended to be public knowledge
- Use the schema to craft valid requests to methods not included in the public API documentation
Go: Registering reflection (only do this in development)
import "google.golang.org/grpc/reflection"
// This line enables reflection — remove it in production
reflection.Register(server)
Go: Conditionally enable reflection based on environment
if os.Getenv("APP_ENV") == "development" {
reflection.Register(server)
}
Python: Reflection in development only
from grpc_reflection.v1alpha import reflection
from your_proto import your_service_pb2
if os.environ.get('APP_ENV') == 'development':
SERVICE_NAMES = (
your_service_pb2.DESCRIPTOR.services_by_name['YourService'].full_name,
reflection.SERVICE_NAME,
)
reflection.enable_server_reflection(SERVICE_NAMES, server)
Check whether reflection is enabled on production gRPC services with:
grpc_cli ls <host>:<port>
# If this returns a list of services, reflection is enabled
Or with grpcurl:
grpcurl -plaintext <host>:<port> list
# Returns services if reflection is on
Vulnerability 3: Missing Server-Side Authentication Interceptors
gRPC’s client-side authentication options are well-documented. The server-side authentication story — where the server validates that incoming requests carry valid credentials — is less prominently covered and frequently missing from implementations.
The result is gRPC servers that accept and process any request from any caller that can reach the network endpoint, with no validation of who the caller is. In a microservices environment where multiple services have network access to the gRPC server, this means any compromised service can make arbitrary calls to any method.
Go: Unary interceptor for JWT validation
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
)
func authInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authorization token")
}
token := strings.TrimPrefix(tokens[0], "Bearer ")
if err := validateJWT(token); err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return handler(ctx, req)
}
server := grpc.NewServer(
grpc.Creds(creds),
grpc.UnaryInterceptor(authInterceptor),
grpc.StreamInterceptor(streamAuthInterceptor),
)
Note that separate interceptors are needed for unary and streaming RPCs — a common mistake is implementing only the unary interceptor and leaving streaming methods unprotected.
Python: Interceptor pattern
import grpc
from grpc import ServicerContext
class AuthInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
metadata = dict(handler_call_details.invocation_metadata)
token = metadata.get('authorization', '')
if not token.startswith('Bearer '):
def abort(ignored_request, context):
context.abort(grpc.StatusCode.UNAUTHENTICATED,
'Missing or invalid token')
return grpc.unary_unary_rpc_method_handler(abort)
if not validate_token(token[7:]):
def abort(ignored_request, context):
context.abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid token')
return grpc.unary_unary_rpc_method_handler(abort)
return continuation(handler_call_details)
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
interceptors=[AuthInterceptor()]
)
For service-to-service communication where you control both ends, mTLS is often cleaner than token-based authentication — the client certificate serves as identity proof and is validated at the TLS layer rather than requiring application-level token handling.
Vulnerability 4: Denial of Service via Large or Deeply Nested Messages
gRPC does not impose default limits on message size or processing complexity. An attacker (or a buggy client) can send extremely large messages or deeply nested protobuf structures that consume excessive memory and CPU, causing the service to slow down or crash.
Protocol buffers support recursive/nested message definitions. A message with fields that contain the same message type can be crafted with arbitrary nesting depth. Processing a deeply nested message requires recursive unmarshalling that consumes stack space proportional to nesting depth, which can cause stack overflow or excessive memory use.
Go: Configuring message size limits
server := grpc.NewServer(
grpc.Creds(creds),
grpc.MaxRecvMsgSize(4 * 1024 * 1024), // 4MB max incoming message
grpc.MaxSendMsgSize(4 * 1024 * 1024), // 4MB max outgoing message
grpc.MaxConcurrentStreams(100), // Limit concurrent streams
)
On the client side, set corresponding limits:
conn, err := grpc.Dial(address,
grpc.WithTransportCredentials(creds),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(4 * 1024 * 1024),
),
)
Python: Message size limits
options = [
('grpc.max_receive_message_length', 4 * 1024 * 1024),
('grpc.max_send_message_length', 4 * 1024 * 1024),
]
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
options=options
)
For services that need to accept large messages from internal systems but should not accept large messages from external-facing APIs, implement size limits in an interceptor that varies the limit based on the authenticated caller’s identity.
Scanning for gRPC Vulnerabilities
Detect plaintext gRPC exposure:
# Check if a gRPC server accepts plaintext connections
grpcurl -plaintext <host>:<port> list
# If this succeeds, TLS is not required
Detect reflection:
grpcurl -plaintext <host>:<port> list
grpcurl <host>:<port> list # with TLS
Test for missing auth: Try calling a method without credentials:
grpcurl -plaintext -d '{}' <host>:<port> your.Service/Method
# If this succeeds without a token, auth is missing
Automated scanning: Tools like grpc-security-scanner and Nuclei templates for gRPC can automate detection of common misconfigurations across a microservices fleet.
Summary Checklist
For any gRPC service going to production:
- TLS configured (
NewServerTLSFromFileor mTLS via service mesh) - gRPC reflection not registered in production builds
- Authentication interceptors applied to both unary and streaming RPCs
-
MaxRecvMsgSizeset to a reasonable limit for the service’s use case -
MaxConcurrentStreamsconfigured to prevent stream exhaustion - Server is not internet-accessible without a gateway/proxy that enforces auth
The pattern across all four vulnerabilities is the same: secure defaults are not the gRPC framework’s defaults. Security requires explicit, intentional configuration choices. The good news is that all of these controls are well-supported by the framework — they just need to be applied.