Skip to main content
The Sidecar is an inspecting proxy. It 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. You run it behind something that already owns the network path and identity, which in most enterprises means Envoy. Envoy reaches it over a unix socket or a TCP port, your choice per lane. Envoy answers reachability. The Sidecar answers what the statement does, and what comes back.
This runs with no gateway, no agent, and no control-plane database. One config file is the whole setup.

Prerequisites

  • The hoop CLI installed. See CLI installation.
  • An Envoy you can add a cluster to, or any proxy that can forward plaintext to a local port.
  • 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:
config.yaml
A socket opens no port, so reachability becomes a filesystem question rather than a network one. The cost is coordination: Envoy and the Sidecar mount the same directory and their uids have to agree. Cheap in a pod spec, awkward across hosts.
Nothing above the transport changes. Policy, 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 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:
config.yaml
Policy, 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.
policy.enforce defaults to false. Without it every lane inspects and audits but denies nothing, so a misconfigured rule cannot take production down on first deploy. Set it to true when you want the rules to bite.
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. Policy Rules covers every rule type, including deferring a match to a Rego policy instead of denying it.

Step 2: Validate before you deploy

Nothing needs to be running. The validator builds every lane and reports every problem in one run:
Each line is the resolved lane, so the rule count includes anything it inherited. A lane with an opa block reads + opa, one with masking reads + masking, and one with enforce: false reads observe-only.

Step 3: Run it

--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:

Step 4: Point Envoy at it

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: A path resolves to nothing, so STRICT_DNS on a pipe: endpoint fails at load.
envoy.yaml
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.
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.

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:
envoy.yaml
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.
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.
With all three legs covered, only the hop the Sidecar reads is ever in the clear: 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.

Step 5: Watch it work

With the lane above in place, a destructive statement never reaches the database:
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. Envoy forwarded the same bytes as opaque TCP and consulted nobody. 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:
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:
A rule written as operations: [call] stops matching CALL and EXECUTE entirely. Write operations: [call, unknown].
Read what the Sidecar recorded:

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:
On a socket deployment, ask the Sidecar’s own namespace what it listens on:
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.
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.

Step 6: Add masking

Masking runs on responses. Turn on detection by naming the entity types your data holds, then say how each is rewritten:
config.yaml
The same query now comes back rewritten:
pii.entities is required and there is no all-entities default. Turning on all 45 recognizers rewrites ordinary numeric columns: nine digits in a legal range is a valid US_SSN as far as any detector can tell, and it fires on about a third of random nine-digit business ids. Name the types your data contains.

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.
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.
The Sidecar binds :15432 and :18080 on the compose network. Neither is published to the host.
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.
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.

Troubleshooting


Next

Policy Rules

Every rule type, deferring a match to Rego, and the findings a policy reads.

Config File Reference

Every section, every rule type, inheritance between lanes, and what startup refuses.

Components and Architecture

How a request flows through the Sidecar, multi-lane and unix-socket deployments, Kubernetes.