> ## Documentation Index
> Fetch the complete documentation index at: https://hoopdev-docs-control-plane-owns-listeners.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# gRPC

> The grpc lane: an in-process HTTP/2 endpoint that decodes, evaluates and re-encodes every RPC

A `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.

```yaml config.yaml theme={null}
listeners:
  - name: ledger
    protocol: grpc
    listen: 0.0.0.0:18443
    upstream: ledger:9000
    identity_header: x-hoop-user      # trust only behind an authenticating proxy
    grpc:
      descriptors: /etc/hoop-inspect/descriptors/ledger.pb
      capture_payload: true
```

***

## The `grpc` block

| Field               | Meaning                                                                                                                                                                                                                                                 |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `descriptors`       | One serialized `FileDescriptorSet` or a list (`protoc --include_imports --descriptor_set_out`, or `buf build -o`). Required for any payload work: schema-less protobuf walking loses values, so no capture, masking or PII scanning happens without it. |
| `capture_payload`   | Renders decoded request and response messages into per-message statements, so payload rules (`pii`, `pattern_match`) and OPA read field values as well as the method path. Requires `descriptors`.                                                      |
| `max_payload_bytes` | Truncates a captured rendering. Masking ignores it: a redactor rewrites decoded fields whatever their size.                                                                                                                                             |
| `strict`            | Refuses what the descriptor set cannot explain; see below.                                                                                                                                                                                              |
| `metadata`          | Request metadata headers exposed to policy, matched case-insensitively. No capture-all, and `authorization`, `cookie` and `proxy-authorization` can never be listed.                                                                                    |

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:

```yaml config.yaml theme={null}
listeners:
  - name: billing
    protocol: grpc
    listen: 127.0.0.1:18443
    upstream: billing:50051
    upstream_tls: {}        # discovery dials with the lane's own TLS facts
```

Then run discovery against that config:

```bash theme={null}
hoop-inspect -grpc-discover billing -grpc-discover-out billing.pb -config config.yaml
```

It picks the lane by name, dials the lane's `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. `-validate` does 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`, or `buf build -o`.
* `spanner` lanes bootstrap the same way.

### Lenient, strict, and the masking carve-out

|                                                   | default (lenient)                                                          | `strict: true`                                                                    |
| ------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Method in a listed set                            | inspected, masked, forwarded                                               | same                                                                              |
| Method in no listed set                           | forwarded with method-level inspection only; the lane logs the degradation | refused **before the upstream is dialed**: `FAILED_PRECONDITION`, naming the path |
| Message that does not decode as its declared type | forwarded uninspected, logged                                              | ends the RPC with `INTERNAL`                                                      |
| Lane with mask rules, either case                 | refused regardless: a redactor must not forward what it cannot decode      | same                                                                              |

`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_resource` matches method identity: `resources: ["/demo.v1.Ledger/ExportAll"]`.
* `grpc_status` matches the outcome in the `grpc-status` trailer: `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, pair `action: defer` with an `opa` block.
* `pii` and `pattern_match` read decoded payloads, so startup refuses them on a lane without `capture_payload`: they would only ever scan the method path and would fire on nothing.

Denials return `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](/setup/configuration/hoop-sidecar/protocols/http#identity): 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:

| Shape               | Client leg                                                  | Config                                                                      |
| ------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------- |
| Behind Envoy        | Envoy terminates TLS, forwards h2c                          | nothing: the lane listens for cleartext HTTP/2                              |
| Standalone, in-pod  | Cleartext h2c, trusted because the hop never leaves the pod | nothing; clients opt in (`grpcurl -plaintext`, `insecure.NewCredentials()`) |
| Standalone, exposed | The lane terminates TLS                                     | `downstream_tls: {cert_file: ..., key_file: ...}`                           |

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 same `http_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:

```yaml envoy.yaml theme={null}
routes:
  - match: { prefix: "/" }
    route:
      cluster: hoop_inspect_grpc
      timeout: 0s               # gRPC convention: the client's grpc-timeout
                                # header sets the deadline, not the router

clusters:
  - name: hoop_inspect_grpc
    type: STRICT_DNS
    connect_timeout: 5s
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}    # the hop to the lane must be h2
    load_assignment:
      cluster_name: hoop_inspect_grpc
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address: { address: hoop-inspect, port_value: 18443 }
```

The gRPC overlay in [`deploy/docker-compose/envoy-stack/grpc/`](https://github.com/hoophq/hoop/tree/main/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`:

```bash theme={null}
./run.sh
docker compose -f docker-compose.yml -f grpc/docker-compose.grpc.yml up -d --wait
./grpc/demo-grpc.sh
```

***

## Next

<CardGroup cols={2}>
  <Card title="Guardrail Rules" icon="shield-halved" href="/setup/configuration/hoop-sidecar/policy-rules">
    `http_resource`, `grpc_status`, PII rules and deferring a match to Rego.
  </Card>

  <Card title="Config File Reference" icon="file-code" href="/setup/configuration/hoop-sidecar/config-file">
    Every listener field, inheritance between lanes, and what startup refuses.
  </Card>
</CardGroup>
