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.
Descriptor sources
An entry is a file on the Sidecar’s filesystem, or an object in Google Cloud Storage:config.yaml
-validate, which performs the real fetch so a wrong object name or a missing IAM binding fails there with the URL in the message. The schema is bound into the endpoint together with the lane’s rules, so the heartbeat reload does not refetch it: a changed descriptors list is drift the process answers with “restart to apply it”, and a new version published behind an unpinned URL is applied by a restart, the way a replaced file at the same path is. One fetch has a two-minute budget covering credential discovery, the token exchange and the read.
Pinning a generation. ?generation=N reads one version of the object, the JSON API’s own parameter, and is the only query parameter accepted; any other one, and any escape the URL parser cannot decode, is refused rather than dropped, so a typo cannot read the current version under a URL that looks pinned. Pin when two restarts must decode against one schema; leave it unpinned when the bucket is the publishing channel and a restart is how a new schema rolls out.
Credentials and permissions. The read is one authenticated GET as a GCP identity. The Sidecar checks two sources in order. A service account key placed inline in GOOGLE_APPLICATION_CREDENTIALS_JSON wins; this is the gateway’s own variable, so one Secret serves both processes. Set but malformed, it fails the fetch naming the variable, never falls through to another identity, and never echoes the key. Otherwise the Sidecar uses Application Default Credentials: Workload Identity on GKE, the attached service account on GCE or Cloud Run, a key file named by GOOGLE_APPLICATION_CREDENTIALS, or gcloud on a laptop. The identity needs storage.objects.get on the object (roles/storage.objectViewer). There is no anonymous read, and a gs:// URL carries no credentials of its own. The sidecar chart README shows the three deployment shapes: a Workload Identity annotation on serviceAccount, the inline key through extraSecret, or a mounted key file plus GOOGLE_APPLICATION_CREDENTIALS.
Validation. A scheme the binary does not link (s3://) is refused at config validation naming what is linked; hoop-inspect and hoop start sidecar link gs://. An object over 512 MiB is refused, since a descriptor set of that size is a wrong object name. What arrives is indexed under the same merge rules as a file, so a stale or malformed artifact fails the same way.
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 artifact it was configured with, a file or a published object, read once when the endpoint is built. A schema fetched live from the policed service at runtime would let it rename fields out from under your mask rules, so no descriptors: auto exists; a gs:// entry is not that: it reads an artifact your CI published and reviewed, not the upstream’s own reflection.
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.
Every gRPC statement also carries the RPC’s identity as metadata, which Rego reads as input.metadata[...]:
A rule keyed on the method,
input.metadata["grpc.method"] == "ReadRows", needs no metadata allowlist entry. Headers you list in the grpc block arrive lower-cased under input.http.headers instead.
OPA and streamed responses
Withcapture_payload: true, each response message becomes its own statement, and each statement reaches OPA. A server-streaming method that returns 50k messages costs 50k OPA calls in series before the client sees the last one. If your Rego only reads requests, turn the response side off: opa.responses: false on the lane, or responses: false in the result of a request decision for one method at a time. The guardrails, masking and the audit trail keep running on every message either way. Config File has both forms.
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.