grpc lane works differently from the relay lanes: the Sidecar terminates HTTP/2 itself (cleartext h2c by default, TLS with downstream_tls), decodes each RPC into statements, evaluates policy per message, and re-encodes responses it masked before forwarding to the upstream. The other protocols arrive as a byte stream a codec reads in flight; gRPC’s multiplexed streams and nested length prefixes rule that shape out, so the lane serves as its own endpoint.
config.yaml
The grpc block
Multiple descriptor sets fit the multi-team shape: each service’s CI ships its own artifact and the lane lists them all. Sets merge at startup, byte-identical shared imports dedupe, and two diverged copies of one file refuse to load naming both artifacts.
No descriptor set yet: bootstrap one with -grpc-discover
When the upstream serves gRPC server reflection, the Sidecar binary produces the artifact for you. Declare the lane first, without a grpc: block; a descriptor-less lane is valid and enforces method-level policy:
config.yaml
upstream with the lane’s upstream_tls (h2c when the block is absent), fetches the schema over reflection (v1, with a v1alpha fallback), prints every method with its maskable field paths, and writes the serialized set. Review the report, commit the artifact, add grpc.descriptors pointing at it, restart.
Discovery is a bootstrap command, and the lane still loads only the pinned file. A schema fetched live at runtime would let the policed service rename fields out from under your mask rules, so no descriptors: auto exists.
Sequencing facts worth knowing:
- Discovery runs against a config whose
descriptors:path does not exist yet; it never builds the lane.-validatedoes build the lane, so run it after the file exists. - The artifact is byte-stable against an unchanged server. Re-run discovery and diff the file to see what the service changed.
- Discovery sends no credentials and gives one run 30 seconds. An upstream that requires authenticated reflection, or one that serves none (Google’s production APIs, the Spanner emulator), answers with an error naming the offline route:
protoc --include_imports --descriptor_set_out, orbuf build -o. spannerlanes bootstrap the same way.
Lenient, strict, and the masking carve-out
strict without descriptors is a startup error: strictness about payloads nothing can decode would refuse every RPC.
Rules on a gRPC lane
A gRPC statement carries its un-normalized RPC path as the resource, so the existing rule types cover it:http_resourcematches method identity:resources: ["/demo.v1.Ledger/ExportAll"].grpc_statusmatches the outcome in thegrpc-statustrailer:statuses: [permission_denied]. It runs response-side, and the body has already reached the client when trailers arrive, so a deny here replaces the final status rather than retracting data. To record outcomes instead, pairaction: deferwith anopablock.piiandpattern_matchread decoded payloads, so startup refuses them on a lane withoutcapture_payload: they would only ever scan the method path and would fire on nothing.
PERMISSION_DENIED with the rule’s message, the shape a gRPC client already handles.
Identity
Behind an authenticating proxy,identity_header names the caller, the same proxy-trust contract as on an HTTP lane: safe only when nothing else can reach the listener. Without the header, a lane terminating TLS falls back to the verified client certificate, the SPIFFE URI SAN where one exists and the subject common name otherwise. With neither, sessions record principal=anonymous.
TLS and the two deployment shapes
grpc is one of two protocols that may terminate the client’s TLS at the lane (the other is postgres), because the lane is its own HTTP/2 endpoint and must present the certificate itself:
The hop to the upstream is h2c by default;
upstream_tls turns it into TLS with the usual ca_file, server_name and client-certificate fields.
The Envoy lane
gRPC is HTTP/2, so Envoy sees more here than on the database lanes: the samehttp_connection_manager and ext_authz filter run, and OPA answers method-level reachability on :path before the Sidecar sees the RPC. Two settings distinguish the lane from a plain HTTP one:
envoy.yaml
deploy/docker-compose/envoy-stack/grpc/ runs all of this: the lane through Envoy and without it, two descriptor sets merged, the strict/lenient contrast, and a standalone stack where the lane owns TLS. Its demo drives each beat with grpcurl:
Next
Guardrail Rules
http_resource, grpc_status, PII rules and deferring a match to Rego.Config File Reference
Every listener field, inheritance between lanes, and what startup refuses.