Safeguard
Application Security

Securing gRPC APIs: mTLS, Interceptors, and Input Validation

CVE-2023-44487 knocked grpc-go services offline with a Rapid Reset flood — a reminder that gRPC ships fast, insecure by default, and needs hardening at every layer.

Safeguard Research Team
Research
6 min read

gRPC ships insecure by default, and most teams don't notice until an audit or an outage forces the question. A brand-new Go service scaffolded with grpc.NewServer() and grpc.Creds(insecure.NewCredentials()) will happily accept plaintext connections in production — the transport-security step is opt-in, not automatic, and it's a default that has landed in real deployments. The cost of skipping it showed up in October 2023 as CVE-2023-44487, "HTTP/2 Rapid Reset," which affected grpc-go and was patched in versions 1.56.3, 1.57.1, and 1.58.3 (tracked under GitHub advisory GHSA-m425-mq94-257g): an attacker opens a flood of streams and immediately cancels each with an RST_STREAM frame, burning server-side work while never breaching configured concurrent-stream limits that were set too high or not set at all. A second flaw, CVE-2023-32732, showed that even metadata handling needs scrutiny — malformed base64 in -bin-suffixed gRPC headers was handled inconsistently between proxies and servers, forcing connection termination. Neither bug required a logic flaw in application code; both exploited gaps between what gRPC does by default and what production actually needs. This post walks through the three layers — mutual TLS, authorization interceptors, and protobuf input validation — that close those gaps.

Why isn't gRPC secure by default?

gRPC is a wire protocol and RPC framework built on HTTP/2 and Protobuf, and its designers left transport security, authentication, and input validation as explicit choices rather than baked-in behavior — the same tradeoff that made frameworks like Express or Flask fast to prototype and easy to misconfigure. In Go, grpc.NewServer() with no credentials option, or an explicit insecure.NewCredentials(), produces a server that accepts unencrypted, unauthenticated connections from anything that can reach the port. This matters more for gRPC than for typical REST services because gRPC is the default choice for internal service-to-service traffic — the calls a team assumes are "inside the perimeter" and therefore lower-risk. That assumption is exactly what mutual TLS and authorization interceptors exist to remove: in a service mesh or microservices architecture, every internal hop is a network boundary an attacker who's landed one compromised pod can traverse, so trust has to be established on every call, not just at the edge.

How does mutual TLS actually secure a gRPC connection?

Mutual TLS (mTLS) requires both the client and the server to present X.509 certificates during the TLS handshake, each verified against a trusted certificate authority, so the connection carries cryptographic proof of identity in both directions instead of just the client trusting the server. In grpc-go this means replacing insecure.NewCredentials() with credentials.NewTLS() configured with ClientAuth: tls.RequireAndVerifyClientCert on the server side and a client certificate plus CA pool on the client side. This is the standard mechanism for service-to-service authentication in gRPC deployments and is why service meshes like those built around SPIFFE/SPIFFE ID issue short-lived workload certificates automatically — mTLS establishes who is calling, cryptographically, before a single line of business logic runs. It does not, on its own, decide what that identity is allowed to do; that's the interceptor's job, covered next. Certificate rotation and CA management are the operational cost of mTLS, which is why automated issuance (SPIFFE, cert-manager, or a mesh's built-in CA) matters as much as the handshake configuration itself.

What do interceptors add on top of mTLS?

Interceptors are gRPC's equivalent of middleware — in Go, UnaryServerInterceptor and StreamServerInterceptor; in Java, ServerInterceptor — and they're the idiomatic place to enforce authorization once mTLS has established a verified identity. A typical pattern: extract the peer's certificate (or a SPIFFE ID encoded in it) from the connection context, or read a JWT passed in call metadata, and check it against a per-method policy before the request reaches handler code. This separates transport identity from application authorization cleanly: mTLS answers "is this a certificate signed by our CA," while the interceptor answers "is this specific identity allowed to call DeleteAccount." Because interceptors run before the handler for every RPC on a server, they're also the natural place to enforce rate limits per caller and to reject malformed metadata early — which is directly relevant to CVE-2023-32732, where inconsistent handling of malformed -bin metadata caused forced disconnects. Validating metadata shape in an interceptor, rather than assuming a downstream proxy already did it, closes that class of gap.

Why does Protobuf need separate input validation?

Protobuf's wire format only guarantees that bytes decode into the shape your .proto file describes — it enforces no semantic constraints. A string email field will happily decode an empty string, a 50MB string, or a value with no @ in it, because Protobuf's job ends at successful deserialization. This is the protobuf-era version of the same lesson classic input-validation defenses teach for web forms and API bodies: schema-valid does not mean safe or well-formed. The community answer is explicit validation frameworks — protoc-gen-validate and its successor protovalidate — which let you annotate .proto fields with constraints (string length, numeric ranges, required fields) that generate validation code run before business logic executes. Skipping this step means every handler has to defensively re-check its own inputs, inconsistently, or trust attacker-controlled fields outright. Static analysis that traces data flow from an RPC's deserialized request fields to the code paths that consume them — the same source-to-sink tracing SAST tools apply to HTTP handlers — can flag exactly this gap: a protobuf field reaching a database query, file path, or shell call with no validation step in between.

Does gRPC Server Reflection create risk in production?

Yes — the gRPC Server Reflection service, when left enabled, exposes your entire .proto schema (every service, method, and message field) to anyone who can reach the port, effectively handing an attacker a complete API map without them writing a single .proto file themselves. Multiple independent security write-ups — including analyses from StackHawk, IBM's PTC Security team, and OneUpTime — converge on the same recommendation: disable reflection in production builds, or gate it behind an authenticated, admin-only channel, and enable it only in development and staging where the discoverability tradeoff is worth the convenience. This is a low-effort, high-value fix precisely because it's a single flag, not an architectural change, yet it's one of the most commonly missed hardening steps because reflection is often left on by default in scaffolding templates and never revisited before a service reaches production traffic.

How Safeguard Helps

None of these gaps — a plaintext grpc.NewServer(), a missing authorization check in an interceptor, or an unvalidated protobuf field flowing straight into a handler — announce themselves at compile time; they surface as reachable, exploitable paths only when someone traces the actual data flow. Safeguard's SAST engine performs source-to-sink dataflow tracing across JavaScript/TypeScript, Python, and Java codebases, which is the same technique that catches an unvalidated field from a deserialized request reaching a sensitive sink, whether that request arrived over REST or gRPC. Safeguard's DAST capability complements that by safely exercising live, verified targets to confirm which endpoints are actually reachable and unauthenticated — the dynamic-testing counterpart to a reflection service or an interceptor that never got wired up. Together, static reachability and dynamic verification give a security team evidence about which of these gRPC hardening gaps are theoretical and which are one open port away from being real.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.