> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.hoop.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Command Line

> Install and configure the hoop.dev CLI to interact with your applications.

## Prerequisites

To get the most out of this guide, you will need to:

* Either [create an account in our managed instance](https://use.hoop.dev) or [deploy your own hoop.dev instance](/docs/setup/deployment/overview)

## Installation

<Tabs>
  <Tab title="MacOS">
    Through homebrew:

    ```bash theme={"dark"}
      brew tap hoophq/brew https://github.com/hoophq/brew.git
      brew install hoop
    ```

    To upgrade it:

    ```bash theme={"dark"}
      brew update && brew upgrade hoop
    ```
  </Tab>

  <Tab title="Linux">
    To install or upgrade:

    ```bash theme={"dark"}
      curl -s -L https://releases.hoop.dev/release/install-cli.sh | sh
    ```
  </Tab>

  <Tab title="Windows">
    <Steps>
      <Step title="Download">
        Download the latest release of [hoop command line](https://github.com/hoophq/hoop/releases) for your architecture (usually **hoop-windows-amd64**) and save it in the **Downloads** folder
      </Step>

      <Step title="Extract">
        Extract the file using **winrar** or **winzip**
      </Step>

      <Step title="Place it in the right folder">
        Move the extracted file `hoop.exe` to the folder `C:\Windows\System32\`
      </Step>

      <Step title="Check the installation">
        Open a terminal session and type `hoop version` if it successfully shows the program’s version it’s installed.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Windows WSL">
    ### Prerequisites

    1. Install the Windows Terminal [https://aka.ms/terminal](https://aka.ms/terminal)
    2. Install Ubuntu WSL [https://ubuntu.com/wsl](https://ubuntu.com/wsl)
    3. Open the Windows Terminal App
    4. Install and open the Ubuntu typing `ubuntu`. It should prompt to create a user in the first time

    ```bash theme={"dark"}
      PS C:\Users\san> ubuntu
      san@DESKTOP-BBQAAEG:~$
    ```

    5. gain root access

    ```bash theme={"dark"}
      sudo su -
    ```

    6. To install or upgrade the hoop.dev command line:

    ```bash theme={"dark"}
      curl -s -L https://releases.hoop.dev/release/install-cli.sh | sh
    ```
  </Tab>

  <Tab title="Manual">
    Check the latest version for your operating system architecture in the [releases page](https://github.com/hoophq/hoop/releases).
  </Tab>
</Tabs>

<Note>
  Already installed? The CLI ships its own version manager under `hoop versions`. Use `hoop versions sync` to match the connected gateway, `hoop versions upgrade` to track the latest release, or `hoop versions install <version>` to pin a specific version. See [hoop versions](/docs/clients/cli-versions) for details.
</Note>

***

## Running the Sidecar

The same binary also runs the Sidecar directly — an inspecting proxy that decodes the wire protocol between a client and a database or an API, evaluates every statement against policy, records an audit trail, and masks sensitive values in the response.

It routes nothing and terminates no client TLS. A client can dial it directly:

```mermaid theme={"dark"}
flowchart LR
    C["client<br/>psql · curl"] -->|TCP or socket| H["Sidecar<br/>is this DELETE allowed?"]
    H --> U["postgres · HTTP API"]
```

Or, if you already run something that owns the network path and identity — Envoy is the common choice in bigger environments — put that in front instead. That's entirely optional, and it's covered at the end of this page in [Optional: Put Envoy in Front](#optional-put-envoy-in-front).

<Note>
  This runs with no gateway, no agent, and no control-plane database. One config file is the whole setup — nothing above needs a hoop.dev account.
</Note>

***

## Before You Start

* A backend to protect: PostgreSQL, SQL Server or an HTTP service.

***

## Step 1: Write a config file

One listener is one upstream. Start with a single Postgres lane and nothing else.

Pick a transport first. One field decides it, and the rest of the file is identical either way:

|              | Unix socket                                           | TCP port                                   |
| ------------ | ----------------------------------------------------- | ------------------------------------------ |
| Config       | `network: unix` + a path                              | omit `network`, `listen` takes `host:port` |
| Reachable by | whoever can open the file                             | anything that can route to the host        |
| Needs        | both processes sharing a directory, and agreeing uids | nothing                                    |
| Fits         | a sidecar beside one workload, one pod                | separate hosts, or a laptop                |

<Tabs>
  <Tab title="Unix socket">
    ```yaml config.yaml theme={"dark"}
    log_level: info

    admin:
      listen: 127.0.0.1:19000     # /healthz, /stats, /config, /api/*

    audit:
      file: "-"                   # JSON lines on stdout

    listeners:
      - name: appdb                            # the name audit rows and rules key on
        protocol: postgres
        network: unix                          # no port is opened at all
        listen: /run/hoop-inspect/pg.sock      # where bytes arrive
        upstream: appdb:5432                   # the real database
        guardrails:
          rules:
            - name: no-destructive-sql
              type: operation
              operations: [drop, delete, truncate]
              message: destructive statements are not permitted on appdb
    ```

    A socket opens no port, so reachability becomes a filesystem question rather than a network one. The cost is coordination: whatever's in front — Envoy is the common choice — and the Sidecar mount the same directory and their uids have to agree. Cheap in a pod spec, awkward across hosts.
  </Tab>

  <Tab title="TCP port">
    ```yaml config.yaml theme={"dark"}
    log_level: info

    admin:
      listen: 127.0.0.1:19000     # /healthz, /stats, /config, /api/*

    audit:
      file: "-"                   # JSON lines on stdout

    listeners:
      - name: appdb               # the name audit rows and rules key on
        protocol: postgres
        listen: 127.0.0.1:15432   # network omitted -> tcp
        upstream: appdb:5432      # the real database
        guardrails:
          rules:
            - name: no-destructive-sql
              type: operation
              operations: [drop, delete, truncate]
              message: destructive statements are not permitted on appdb
    ```

    Nothing to mount and nothing to chown, which is why the compose stack defaults to this. Bind loopback where the Sidecar and whatever's in front of it share a network namespace: a `0.0.0.0` bind is reachable by anything that can route to the host, and a NetworkPolicy narrows that without removing it.
  </Tab>
</Tabs>

Nothing above the transport changes. Guardrails, masking, audit and `upstream_tls` behave the same way, because the gate reads a `net.Conn` and never asks what kind it is. See [Transport](/docs/setup/configuration/hoop-sidecar/config-file#transport) for the full comparison.

### Another protocol is one field

`protocol` picks the codec and nothing else in the lane changes with it. A SQL Server lane is the same shape as the Postgres one above:

```yaml config.yaml theme={"dark"}
listeners:
  - name: mssqldb
    protocol: mssql             # postgres | mysql | mssql | mongodb | grpc | http
    listen: 127.0.0.1:11433     # network: unix works here too
    upstream: mssql:1433
    guardrails:
      rules:
        - name: no-destructive-tsql
          type: operation
          operations: [drop, delete, truncate]
          message: destructive statements are not permitted on mssqldb
```

Guardrails, masking and audit behave as they do on a Postgres lane. Two differences belong to the protocol: the client's TLS is TDS 8.0, which Envoy terminates with a plain listener, and the hop to SQL Server stays plaintext because the Linux build accepts no TLS shape the Sidecar can originate. Both are covered in [Kerberos and SQL Server](/docs/setup/configuration/hoop-sidecar/kerberos). What each protocol decodes, refuses and masks has a page of its own under [Protocols](/docs/setup/configuration/hoop-sidecar/protocols/postgres).

<Warning>
  **A lane always enforces.** There is no observe-only switch: a rule you write denies from the first statement that matches it. Deploy a new rule set against a staging listener, or [validate](#step-2-validate-before-you-deploy) and read `/api/events?kind=violation` there, before it fronts production.
</Warning>

<Note>
  `type: operation` matches the statement's most consequential **effect**, so `WITH d AS (DELETE FROM customers RETURNING *) SELECT count(*) FROM d` counts as a `delete` and the rule above catches it. [Guardrail Rules](/docs/setup/configuration/hoop-sidecar/policy-rules) covers every rule type, including deferring a match to a Rego policy instead of denying it.
</Note>

***

## Step 2: Validate before you deploy

Nothing needs to be running. The validator builds every lane and reports every problem in one run:

```bash theme={"dark"}
hoop start sidecar --config config.yaml --validate
```

```
config OK: 1 listener(s)
  appdb            postgres  1 rule(s)
```

Each line is the **resolved** lane, so the rule count includes anything it inherited. A lane with an `opa` block reads `+ opa`, and one with masking reads `+ masking`.

***

## Step 3: Run it

```bash theme={"dark"}
hoop start sidecar --config config.yaml
```

`--config` also reads `HOOP_SIDECAR_CONFIG`, which is the shape a Kubernetes deployment wants: mount the ConfigMap, set the variable, pass no arguments.

Check it came up:

```bash theme={"dark"}
curl -s localhost:19000/healthz     # ok
curl -s localhost:19000/config | python3 -m json.tool
```

***

## Step 4: Watch it work

With the lane above in place, a destructive statement never reaches the database:

```bash theme={"dark"}
PGSSLMODE=disable psql -h 127.0.0.1 -p 15432 -U appuser -d appdb \
  -c 'DELETE FROM customers WHERE id = 1;'
```

```
FATAL:  destructive statements are not permitted on appdb
```

That is a real pgwire `ErrorResponse` carrying the message you wrote in `config.yaml`, so the developer reads it in psql instead of watching a socket drop.

The rule catches more than its three verbs suggest, because `Operation` is the worst effect rather than the leading word. A delete hidden inside a CTE trips it:

```bash theme={"dark"}
PGSSLMODE=disable psql -h 127.0.0.1 -p 15432 -U appuser -d appdb \
  -c 'WITH d AS (DELETE FROM customers RETURNING *) SELECT count(*) FROM d;'
```

```
FATAL:  destructive statements are not permitted on appdb
```

<Note>
  With Envoy in front (see [Optional: Put Envoy in Front](#optional-put-envoy-in-front)), dial whatever host and port Envoy publishes instead — the Sidecar's behavior is identical either way.
</Note>

<Warning>
  `CALL` and `EXECUTE` report `unknown` rather than `call`, because their bodies live in the catalog and no parser can say what they touch. The rule above forwards `CALL purge()`. Add `unknown` where that matters:

  ```yaml theme={"dark"}
  operations: [drop, delete, truncate, unknown]
  ```

  A rule written as `operations: [call]` stops matching `CALL` and `EXECUTE` entirely. Write `operations: [call, unknown]`.
</Warning>

Read what the Sidecar recorded:

```bash theme={"dark"}
curl -s localhost:19000/stats                  | python3 -m json.tool
curl -s 'localhost:19000/api/sessions?limit=1' | python3 -m json.tool
```

```json theme={"dark"}
{"sessions": [
  {"id": "98203ccc…", "principal": "anonymous", "protocol": "postgres",
   "connection": "appdb", "duration_ms": 11,
   "statement_count": 2, "denied_count": 1, "masked_count": 0, "verdict": "denied"}
]}
```

### Confirm which transport bound

`/stats` reports the address each lane bound, not the string you configured, so it tells you what happened. A path means a socket, a `host:port` means TCP:

```json theme={"dark"}
{"listeners": [
  {"name": "appdb",   "addr": "/run/hoop-inspect/pg.sock",   "active": 0, "total": 4},
  {"name": "httpbin", "addr": "/run/hoop-inspect/http.sock", "active": 0, "total": 5}
]}
```

On a socket deployment, ask the Sidecar's own namespace what it listens on:

```bash theme={"dark"}
netstat -ltn        # or: ss -ltn
```

```
tcp  0  0  127.0.0.11:41515  0.0.0.0:*  LISTEN     docker's internal resolver
tcp  0  0  :::19000          :::*       LISTEN     the admin API
```

The admin port is the only entry left. A TCP deployment lists `:::15432` and `:::18080` beside it. From a peer, `nc -z -w2 hoop-inspect 15432` reports closed or open to match.

<Warning>
  Check with `nc`, not `(echo > /dev/tcp/host/port)`. The latter is a bash builtin, and under BusyBox or dash it fails with no such device and calls every port closed, including open ones. It looks like a passing check and proves nothing.
</Warning>

***

## Step 5: Add masking

Masking runs on responses. Detection needs no setup — every supported entity is enabled unless a `pii` section narrows the list — so one block says what gets rewritten and how:

```yaml config.yaml theme={"dark"}
mask:
  rules:
    - {name: emails, entities: [EMAIL_ADDRESS], strategy: redact}
    - {name: ssn,    entities: [US_SSN], strategy: partial, keep_last: 4}
```

The same query now comes back rewritten:

```
     name     |          email           |     ssn     |          iban
--------------+--------------------------+-------------+------------------------
 Ada Lovelace | [REDACTED:EMAIL_ADDRESS] | ***-**-6789 | ******************5432
 Grace Hopper | [REDACTED:EMAIL_ADDRESS] | ***-**-4321 | ******************3000
```

`entities` is a list, so one rule can cover several types that share a strategy: `{name: ids, entities: [US_SSN, BR_CPF], strategy: hash}`.

<Warning>
  Only the types a `mask` rule names are rewritten, and `US_SSN` is the one to be careful with: it carries no checksum, so nine digits in a legal range is a valid SSN as far as any detector can tell, and it fires on about a third of random nine-digit business ids. Where a lane carries ids like that, mask the column by name instead, and add a `pii` section naming the types your data really holds:

  ```yaml config.yaml theme={"dark"}
  pii:
    entities: [EMAIL_ADDRESS, CREDIT_CARD, BR_CPF, IBAN_CODE]
  ```
</Warning>

***

## Run the whole thing on your laptop

The repository ships a compose stack that runs all of this end to end: Envoy terminating TLS, OPA answering reachability, the Sidecar behind both, a seeded Postgres and an HTTP service behind that. Needs `docker`, `curl`, `openssl` and `python3`.

```bash theme={"dark"}
git clone https://github.com/hoophq/hoop
cd hoop/deploy/docker-compose/envoy-stack

./run.sh       # cert, sidecar image, compose up. First run takes a minute.
./demo.sh      # walks every lane and prints the audit trail
./run.sh down  # tear down, including volumes
```

The stack ships both transports. TCP is the default because it needs no shared volume; the overlay swaps in sockets once the certs exist and the image is built.

<Tabs>
  <Tab title="TCP (default)">
    ```bash theme={"dark"}
    ./run.sh
    ./demo.sh
    ```

    The Sidecar binds `:15432` and `:18080` on the compose network. Neither is published to the host.
  </Tab>

  <Tab title="Unix socket (overlay)">
    ```bash theme={"dark"}
    export COMPOSE_FILE=docker-compose.yml:uds/docker-compose.uds.yml
    docker compose up -d --wait

    ./demo.sh                     # same lanes, same policy, now over sockets
    docker compose down -v        # tear down
    ```

    `COMPOSE_FILE` applies to every `docker compose` in that shell, `demo.sh` included, so the overlay stays selected without repeating the flags. Prefer it to a `CF="-f a -f b"` variable: that idiom relies on unquoted word splitting, which bash does and zsh does not, so on zsh the whole string arrives as one argument.

    Confirm it took effect:

    ```bash theme={"dark"}
    docker compose exec -T envoy ls -l /run/hoop-inspect/
    #  srwxrwxr-x 1 10001 envoy 0 http.sock
    #  srwxrwxr-x 1 10001 envoy 0 pg.sock

    docker compose exec -T client sh -c 'nc -z -w2 hoop-inspect 15432 && echo OPEN || echo closed'
    #  closed
    ```
  </Tab>
</Tabs>

| Port  | Serves                                                            |
| ----- | ----------------------------------------------------------------- |
| 8443  | Envoy HTTPS, to the HTTP lane                                     |
| 5433  | Envoy TCP, to the Postgres lane                                   |
| 19000 | relay admin: `/healthz`, `/stats`, `/config`, `/events`, `/api/*` |
| 9901  | Envoy admin                                                       |

Inside the compose network that Postgres listener is `envoy:5432`. The host publishes it on 5433, because a laptop tends to have something on 5432 already. Those are Envoy's ports and the overlay leaves them alone: it removes the Sidecar's two, which were never published to the host.

<Note>
  SQL Server runs in two stacks of its own, and what separates them is who terminates the client's TLS. `deploy/docker-compose/envoy-stack/mssql/` (`hoopinspect/scripts/dev/mssql-stack.sh`) runs SQL Server 2022 over TDS 8.0, with Envoy terminating, a Samba AD DC and a client holding a ticket. `deploy/docker-compose/envoy-stack/mssql2019/` (`mssql2019-stack.sh`) runs TDS 7.4 with no Envoy at all, covering the encrypted login; its `MSSQL_TAG` accepts 2017, 2019 and 2022. Microsoft publishes no arm64 image, so first boot on Apple Silicon takes minutes. See [Kerberos and SQL Server](/docs/setup/configuration/hoop-sidecar/kerberos#local-testing).
</Note>

***

## Troubleshooting

| Symptom                                                      | Check                                                                                                                                                                  |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A rule you wrote never fires                                 | `curl -s localhost:19000/config` and read the lane's resolved `rules`. A rule naming `operations: [call]` stopped matching: `CALL` and `EXECUTE` now report `unknown`. |
| Masking leaves a value alone                                 | The detector may refuse it. Add a column rule, or check that no `pii.entities` list is narrowing the type out.                                                         |
| The Sidecar refuses to start                                 | It prints every config problem at once, each naming its lane                                                                                                           |
| psql says SSL is required                                    | Set `PGSSLMODE=disable`, or terminate the client's TLS in Envoy with [`postgres_proxy`](#terminating-client-tls). The Sidecar reads plaintext either way.              |
| Envoy returns 503 with `flags=UF`                            | On a socket lane, Envoy lacks write permission on the socket file. Check `ls -l` on the directory and the Sidecar's umask. On TCP, check the cluster address.          |
| Everything is allowed                                        | Read the lane's resolved `rules` at `/config`. A rule set that reached no lane, or a `guardrails` block still written as `policy`, allows everything.                  |
| `bind: permission denied` at startup                         | The Sidecar cannot write to the socket directory. Chown it, or set `fsGroup` on the pod.                                                                               |
| `is a live socket; another relay is already listening on it` | A second Sidecar tried to bind a path in use. Stop the first one.                                                                                                      |
| Envoy rejects the cluster config                             | A `pipe:` endpoint needs `type: STATIC`; `STRICT_DNS` tries to resolve a filesystem path. A `socket_address` endpoint needs `STRICT_DNS` or `STATIC` with an IP.       |

***

## Optional: Put Envoy in Front

Everything above works with a client dialing the Sidecar directly. If you already run something that owns the network path and identity — Envoy is the common choice in bigger environments — you can put it in front instead. None of the steps above change; this only adds a hop.

```mermaid theme={"dark"}
flowchart LR
    C["client<br/>psql · curl"] -->|TLS| E["envoy<br/>terminates TLS"]
    E -.->|ext_authz| O["OPA<br/>can alice reach it?"]
    E -->|socket or port| H["Sidecar<br/>is this DELETE allowed?"]
    H --> U["postgres · HTTP API"]
```

Envoy answers reachability. The Sidecar answers what the statement does, and what comes back.

The Sidecar is an ordinary upstream. There is no ext\_proc, no WASM, no custom Envoy filter to install. You change the cluster your listener already routes to.

The cluster shape follows the transport you picked in Step 1:

| Transport   | Cluster `type` | Endpoint address                          |
| ----------- | -------------- | ----------------------------------------- |
| Unix socket | `STATIC`       | `pipe: { path: … }`                       |
| TCP port    | `STRICT_DNS`   | `socket_address: { address, port_value }` |

A path resolves to nothing, so `STRICT_DNS` on a `pipe:` endpoint fails at load.

<Tabs>
  <Tab title="Unix socket">
    ```yaml envoy.yaml theme={"dark"}
    static_resources:
      listeners:
        - name: postgres_ingress
          address:
            socket_address: { address: 0.0.0.0, port_value: 5432 }
          filter_chains:
            - filters:
                - name: envoy.filters.network.tcp_proxy
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
                    stat_prefix: ingress_pg
                    cluster: hoop_inspect_pg

      clusters:
        - name: hoop_inspect_pg
          type: STATIC
          connect_timeout: 5s
          load_assignment:
            cluster_name: hoop_inspect_pg
            endpoints:
              - lb_endpoints:
                  - endpoint:
                      address:
                        pipe: { path: /run/hoop-inspect/pg.sock }
    ```

    Envoy has no pgwire parser, so this lane is plain `tcp_proxy`. Every byte reaches the Sidecar unexamined, which is the reason the Sidecar earns its place here.

    <Warning>
      **Two permission traps, and neither produces a useful error.**

      *Creating the socket.* The Sidecar needs write permission on the directory. A volume that mounts root-owned against a non-root image fails with `bind: permission denied`. Chown the directory before the Sidecar starts, or set `fsGroup` on a Kubernetes pod.

      *Connecting to it.* `connect()` on a unix socket requires **write** permission on the socket file, not read. Go creates a listening socket at `0777 &^ umask`, and the usual 022 clears exactly the group-write bit Envoy needs. Envoy then reports `flags=UF` and `upstream_cx_connect_fail` while the cluster still shows healthy, because the endpoint resolved. Run the Sidecar with Envoy's gid and `umask 0002`.
    </Warning>
  </Tab>

  <Tab title="TCP port">
    ```yaml envoy.yaml theme={"dark"}
    static_resources:
      listeners:
        - name: postgres_ingress
          address:
            socket_address: { address: 0.0.0.0, port_value: 5432 }
          filter_chains:
            - filters:
                - name: envoy.filters.network.tcp_proxy
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
                    stat_prefix: ingress_pg
                    cluster: hoop_inspect_pg

      clusters:
        - name: hoop_inspect_pg
          type: STRICT_DNS
          connect_timeout: 5s
          load_assignment:
            cluster_name: hoop_inspect_pg
            endpoints:
              - lb_endpoints:
                  - endpoint:
                      address:
                        socket_address: { address: hoop-inspect, port_value: 15432 }
    ```

    Nothing to mount. The Sidecar's port is reachable by anything that can route to it, so keep it off any interface a client can find.
  </Tab>

  <Tab title="MSSQL lane">
    TDS 8.0 is TCP, then TLS, then the protocol, so Envoy terminates it with an ordinary `DownstreamTlsContext` and needs no TDS awareness.

    ```yaml envoy.yaml theme={"dark"}
    static_resources:
      listeners:
        - name: mssql_ingress
          address:
            socket_address: { address: 0.0.0.0, port_value: 1433 }
          filter_chains:
            - transport_socket:
                name: envoy.transport_sockets.tls
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
                  common_tls_context:
                    tls_certificates:
                      - certificate_chain: { filename: /etc/envoy/certs/server.crt }
                        private_key: { filename: /etc/envoy/certs/server.key }
              filters:
                - name: envoy.filters.network.tcp_proxy
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
                    stat_prefix: ingress_mssql
                    cluster: hoop_inspect_mssql
                    idle_timeout: 3600s   # a session idles between keystrokes

      clusters:
        - name: hoop_inspect_mssql
          type: STRICT_DNS
          connect_timeout: 5s
          load_assignment:
            cluster_name: hoop_inspect_mssql
            endpoints:
              - lb_endpoints:
                  - endpoint:
                      address:
                        socket_address: { address: hoop-inspect, port_value: 11433 }
    ```

    Clients connect with `Encrypt=strict`. A TDS 7.x client wraps its TLS handshake inside `0x12` packets, which Envoy cannot speak, so that lane skips Envoy and the Sidecar takes the connection directly: `ENCRYPT_OFF` encrypts the login packet alone and leaves every statement readable. See [Kerberos and SQL Server](/docs/setup/configuration/hoop-sidecar/kerberos#tds-7x-encrypts-the-login-and-nothing-else).
  </Tab>

  <Tab title="HTTP lane">
    On HTTP you keep your existing `ext_authz` filter. Only the route's cluster changes: it points at the Sidecar instead of at the service.

    ```yaml envoy.yaml theme={"dark"}
    route_config:
      name: local_route
      virtual_hosts:
        - name: inspect
          domains: ["*"]
          routes:
            - match: { prefix: "/" }
              route:
                cluster: hoop_inspect_http     # was: the service cluster
                timeout: 60s

    http_filters:
      # Unchanged. OPA still answers reachability before anything is forwarded.
      - name: envoy.filters.http.ext_authz
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
          transport_api_version: V3
          failure_mode_allow: false
          grpc_service:
            envoy_grpc: { cluster_name: opa }
            timeout: 2s

      - name: envoy.filters.http.router
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

    clusters:
      - name: hoop_inspect_http
        type: STATIC                          # STRICT_DNS for a TCP lane
        connect_timeout: 5s
        load_assignment:
          cluster_name: hoop_inspect_http
          endpoints:
            - lb_endpoints:
                - endpoint:
                    address:
                      pipe: { path: /run/hoop-inspect/http.sock }
    ```

    Envoy already terminated the client's TLS on the HTTPS listener, so this lane carries plaintext to the Sidecar and there is nothing extra to configure.
  </Tab>
</Tabs>

### Setting up the socket directory

On a unix-socket lane, the directory needs an owner both Envoy and the Sidecar can work with. In Docker Compose that is a one-shot init container:

```yaml docker-compose.yml theme={"dark"}
socket-dir:
  image: alpine:3.20
  command:
    - sh
    - -c
    - |
      mkdir -p /run/hoop-inspect
      chown 10001:101 /run/hoop-inspect     # relay uid, envoy gid
      chmod 2775 /run/hoop-inspect          # setgid: sockets inherit the group
  volumes:
    - inspect-sockets:/run/hoop-inspect

hoop-inspect:
  user: "10001:101"
  entrypoint: ["sh", "-c", "umask 0002 && exec /usr/local/bin/hoop-inspect -config /etc/hoop-inspect/config.yaml"]
  volumes:
    - inspect-sockets:/run/hoop-inspect
  depends_on:
    socket-dir: { condition: service_completed_successfully }
```

The setgid bit on the directory and `umask 0002` on the Sidecar are what make the sockets group-writable, which is the permission Envoy needs to connect. On Kubernetes, `fsGroup` on the pod's securityContext does the same job — see [Sharing a socket with Envoy](/docs/install/kubernetes#sharing-a-socket-with-envoy).

**Restarting after an unclean exit.** Go unlinks the socket when the listener closes, so SIGTERM leaves nothing behind. A SIGKILL, an OOM kill or `docker kill` skips that and the file outlives the process. The Sidecar reclaims it at startup by dialing the path: a socket nothing answers on gets unlinked with a warning, and one that answers is left alone while the bind fails, naming the conflict. Two relays sharing a socket would split a client's connections between them at random.

### Terminating client TLS

The Sidecar reads plaintext. Something in front has to decrypt whatever the client encrypted before the gate sees a statement; the Sidecar terminates **no** downstream TLS.

On the HTTP lane Envoy already does it, because an HTTPS listener terminates TLS by definition. On the Postgres lane the stock `tcp_proxy` does not, which is why the client connects with `PGSSLMODE=disable`. An MSSQL lane is the easy case: TDS 8.0 hands Envoy an ordinary TLS-on-connect handshake, which the tab above terminates.

To encrypt the client's Postgres leg, terminate it in Envoy with the `postgres_proxy` filter and a `starttls` transport socket:

```yaml envoy.yaml theme={"dark"}
filter_chains:
  - transport_socket:
      name: envoy.transport_sockets.starttls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.StartTlsConfig
        tls_socket_config:
          common_tls_context:
            tls_certificates:
              - certificate_chain: { filename: /etc/envoy/certs/pg.crt }
                private_key: { filename: /etc/envoy/certs/pg.key }
    filters:
      - name: envoy.filters.network.postgres_proxy
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.postgres_proxy.v3alpha.PostgresProxy
          stat_prefix: pg
          terminate_ssl: true
      - name: envoy.filters.network.tcp_proxy
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
          stat_prefix: tcp
          cluster: hoop_inspect_pg
```

Postgres negotiates TLS in-band: the client sends an `SSLRequest` packet and waits for a one-byte reply, which is why this needs the `starttls` socket rather than a plain `DownstreamTlsContext`. The client then connects with `PGSSLMODE=require`, Envoy decrypts, and the Sidecar receives the plaintext it needs.

<Warning>
  `postgres_proxy` ships only in the **contrib** image (`envoyproxy/envoy-contrib`), and Envoy marks it experimental and not hardened. The stock `envoyproxy/envoy` image rejects the config with `could not find @type … PostgresProxy`. On Envoy 1.33 the field is `terminate_ssl: true`; newer versions deprecate it in favor of `downstream_ssl: REQUIRE`, so check which your image accepts.
</Warning>

With all three legs covered, only the hop the Sidecar reads is ever in the clear:

| Leg              | Encrypted by                                                                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| client → Envoy   | Envoy, via `starttls` + `postgres_proxy` (HTTPS listener on the HTTP lane, plain `DownstreamTlsContext` on an MSSQL one) |
| Envoy → relay    | nothing, and it must stay that way: the gate parses these bytes                                                          |
| relay → database | the Sidecar, via [`upstream_tls`](/docs/setup/configuration/hoop-sidecar/config-file#upstream-tls)                            |

Keep the middle leg on loopback or a unix socket. It carries decrypted traffic by design, and a socket is the tighter boundary because no port exists to reach.

***

## Next

<CardGroup cols={2}>
  <Card title="Guardrail Rules" icon="shield-halved" href="/docs/setup/configuration/hoop-sidecar/policy-rules">
    Every rule type, deferring a match to Rego, and the findings a policy reads.
  </Card>

  <Card title="Config File Reference" icon="file-code" href="/docs/setup/configuration/hoop-sidecar/config-file">
    Every section, every rule type, inheritance between lanes, and what startup refuses.
  </Card>

  <Card title="Components and Architecture" icon="sitemap" href="/docs/setup/configuration/hoop-sidecar/components">
    How a request flows through the Sidecar, multi-lane and unix-socket deployments, Kubernetes.
  </Card>
</CardGroup>
