postgres lane decodes the PostgreSQL v3 frontend/backend protocol: every statement a client sends, and every row the server returns. It is the lane the compose stack demonstrates and the one Running the Sidecar builds step by step.
config.yaml
What the codec reads
Two message types carry SQL, and the codec reads both:
The codec skips everything else by length, so a bulk
COPY stream costs no memory. It reassembles a statement split across TCP segments before classifying it.
The codec splits a multi-statement Q message with PostgreSQL’s own lexical rules (dollar quoting, standard-conforming strings, nested comments) and evaluates each statement on its own. Operation is the statement’s most consequential effect, so a DELETE hidden inside a CTE classifies as delete; see the worked example.
TLS on each leg
pgwire negotiates TLS in-band: the client sends an 8-byteSSLRequest and waits for a one-byte reply. That shapes both ends of the lane.
A
postgres lane can terminate the client’s TLS itself because a generic
TLS proxy cannot speak pgwire’s in-band exchange:
config.yaml
PGSSLMODE=require and the gate still reads plaintext, because the lane decrypts what it terminates.
Two upstream details:
- A refusal fails the connection. If the server answers
Nto theSSLRequest, the lane errors out instead of downgrading. You asked for an encrypted hop, and a downgrade would send credentials in the clear without telling you. - The Sidecar strips channel binding. It removes
SCRAM-SHA-256-PLUSfrom the server’s SASL offer, because channel binding ties SCRAM to a single TLS session and a terminating relay has two. PlainSCRAM-SHA-256remains and authenticates the same password against the same verifier, so you change no credential and no server setting.
Trace ids from the startup packet
The lane reads the client’sStartupMessage before any statement. It records user as the principal, in the audit trail and in OPA’s input.context.principal. By default it also records every setting the client sends in options, so a caller can stamp a trace id on a connection with nothing but libpq and no Sidecar config:
session_end record carry the value in metadata, and OPA reads it as input.context["postgres.option.claude.session.id"]:
current_setting('claude.session.id', true) returns it.
postgres block:
config.yaml
startup_metadata: written with no value is refused at startup, because it reads as absent: the opposite of what it looks like.
A source the client did not send records no key. A value longer than 256 bytes is cut at a character boundary.
The default records whatever the client puts in
options, search_path and application settings included, in the audit trail, in what OPA receives and in the Sidecar’s process log, which your log pipeline ships under its own retention and access rules. audit.redact_statements covers statement text, not metadata. If your clients may carry a secret in options, name the settings you want, or write [].options parameter unless ignore_startup_parameters lists it, and listing it drops the value before the database sees it. The Sidecar still records it.
Masking
Every row and column in a pgwireDataRow is length-prefixed, so the codec re-frames: it rebuilds each message around the rewritten values, and a mask that grows or shrinks a value cannot desynchronize the client. Both rule shapes work:
RowDescription, and wherever the protocol names its values they beat detection.
Denials
A denied statement returns a real pgwireErrorResponse carrying the rule’s message, so the developer reads it in psql instead of watching the socket drop:
FATAL rather than ERROR because the connection closes with the denial; ERROR would leave psql waiting for a ReadyForQuery that never arrives.
The Envoy lane
Envoy has no pgwire parser, so the lane is plaintcp_proxy and the Sidecar sees every byte Envoy could not examine:
envoy.yaml
postgres_proxy filter with a starttls transport socket. Terminating client TLS has the full shape and its caveats.
Try it end to end with the compose stack in deploy/docker-compose/envoy-stack/: its appdb lane runs this protocol with upstream_tls on and masking live.
Next
Running the Sidecar
Builds a Postgres lane from zero, behind Envoy, with the compose stack to prove it.
Guardrail Rules
Every rule type this lane evaluates, including deferring a match to Rego.