Skip to content

gRPC API Discovery

Saullo Carvalho Castelo Branco edited this page Jun 16, 2026 · 1 revision

gRPC API Discovery

This tutorial walks through discovering a gRPC API with Vespasian and reconstructing its .proto schema via the Server Reflection Protocol. It covers both the reflection-enabled path (you get a full .proto) and the reflection-disabled / auth-gated path (you get a structured finding instead of a schema).

gRPC is opt-in. Unlike REST, GraphQL, and WSDL, gRPC is not auto-detected — its binary HTTP/2 framing doesn't carry the signals the auto-detector keys on. You must select it explicitly with --api-type grpc, or use the dedicated vespasian probe grpc reflection command.

Prerequisites

  • A built vespasian binary (make build, or go install github.com/praetorian-inc/vespasian/cmd/vespasian@latest).
  • A gRPC target. This tutorial uses the bundled test server in test/grpc-server/, which exposes UserService, OrderService, and AccountService (including a server-streaming ListUsers) with reflection enabled.

Start the test server in one terminal:

make -C test/grpc-server run
# listening on localhost:50051 with Server Reflection registered

Sanity-check it with grpcurl:

grpcurl -plaintext localhost:50051 list
# grpc.reflection.v1.ServerReflection
# lab.v1.AccountService
# lab.v1.OrderService
# lab.v1.UserService

Scenario 1 — Reflection enabled

When a server has reflection registered, Vespasian asks it to describe its own services, methods, and message types, walks the transitive FileDescriptorProto import graph, and renders proto3 source.

One-shot probe

The quickest path is the standalone probe, which doesn't need a capture file:

vespasian probe grpc reflection http://localhost:50051 \
  --dangerous-allow-private \
  -o api.proto -v
  • --dangerous-allow-private is required here because localhost is a private address and SSRF protection blocks private targets by default. Never use this flag against production targets.
  • The port (:50051) is required — gRPC commonly runs on non-standard ports.
  • For a TLS endpoint, use an https:// (or grpcs://) URL; the port defaults to 443.

Verbose output:

probing http://localhost:50051 for gRPC reflection
discovered 3 service(s)

api.proto now contains the reconstructed schema, for example:

syntax = "proto3";

package lab.v1;

service UserService {
  rpc GetUser ( .lab.v1.GetUserRequest ) returns ( .lab.v1.User );
  rpc ListUsers ( .lab.v1.ListUsersRequest ) returns ( stream .lab.v1.User );
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

message GetUserRequest {
  string id = 1;
}
// ... OrderService, AccountService, and their messages follow

(Exact formatting is whatever protoprint emits; the server-streaming ListUsers is rendered with stream on its return type.)

Notes on the output:

  • It's deterministic: files are sorted and elements within each file are sorted, so re-running produces identical output.
  • The well-known google/protobuf/* files are omitted — any consumer (protoc, buf generate) already has them.
  • The reflection service itself (grpc.reflection.v1.ServerReflection) is filtered out; only your user-facing services appear.

In the scan / generate pipeline

You can also reach gRPC through the normal two-stage pipeline. Because gRPC isn't auto-detected, pass --api-type grpc:

# One step
vespasian scan https://grpc.example.com:443 --api-type grpc -o api.proto

# Or two stages
vespasian crawl https://grpc.example.com:443 -o capture.json
vespasian generate grpc capture.json -o api.proto

generate grpc requires reflection descriptors to be present in the capture. gRPC's wire format strips field names, so traffic-only .proto inference is not supported — a capture with no reflection data yields no spec.

Scenario 2 — Reflection disabled or auth-gated

Many production servers ship with reflection turned off, or gate it behind authentication. Vespasian handles this gracefully: instead of crashing or returning an empty result, it reports why reflection was unavailable, using the gRPC status code the server returned.

Point the probe at a server without reflection registered:

vespasian probe grpc reflection https://grpc-no-reflection.example.com:443

Representative outcomes:

Server response gRPC status What Vespasian reports
Reflection service not registered Unimplemented reflection not available on <url> (gRPC Unimplemented)
Reflection requires credentials Unauthenticated reflection not available on <url> (gRPC Unauthenticated)
Caller lacks permission PermissionDenied reflection not available on <url> (gRPC PermissionDenied)
Not a gRPC server / ambiguous network error reflection not available on <url> (no structured reason)

In the pipeline (scan/generate), the same condition surfaces on the classified endpoint as a GRPCReflectionResult with reflection_enabled: false and a reflection_unavailable_reason field, so it's visible in the capture even though no .proto is produced.

Auth-gated reflection is detected, not bypassed

Vespasian's reflection probe does not currently send call credentials or metadata. So a server that requires auth for reflection is detected and reported as Unauthenticated/PermissionDenied — it is not bypassed, and no schema is reconstructed. Credentialed reflection is a possible future enhancement.

Discovering gRPC from captured traffic (without reflection)

Even when you can't reconstruct a schema, the classifier can still flag gRPC endpoints in captured/imported traffic (e.g. a Burp or proxy capture). It scores requests on:

  • application/grpc / application/grpc-web* content-type (strongest signal),
  • grpc-status / grpc-message trailer headers,
  • a POST to a /<pkg.qualified.Service>/<Method> path,

with content-type + trailers together giving the highest confidence. This tells you a gRPC surface exists and where it is, even if reflection is off — useful for scoping further manual testing.

Troubleshooting

Symptom Cause / fix
crawl exits with an error and captures nothing against localhost SSRF protection blocked a private seed. Add --dangerous-allow-private (non-production only).
reflection not available (gRPC Unimplemented) The server has reflection disabled. You can't reconstruct the schema via Vespasian; obtain the .proto out-of-band.
generate grpc produces no output The capture has no reflection descriptors. Use vespasian probe grpc reflection against a live server, or scan --api-type grpc.
Connection hangs / times out Wrong port or TLS mismatch. gRPC often uses :50051; use https:///grpcs:// for TLS endpoints. Adjust --timeout if needed.

See also

  • README → API Type SupportgRPC Classification Heuristics and gRPC Server Reflection
  • README → CLI Referencevespasian probe
  • test/grpc-server/README.md — the bundled test target's services and operations

Clone this wiki locally