Skip to main content
A clickhouse lane decodes ClickHouse’s native TCP protocol, used by clickhouse-client and native Go, Java and Python drivers. It reads Query packets before ClickHouse executes them and rebuilds typed result blocks when a mask changes a value. Use the native lane in front of ClickHouse port 9000 for plaintext or its configured secure native port, commonly 9440, for TLS:
config.yaml
Validate before binding the port:
Then start the lane and point the client at its listen address:
A native driver uses the same address. For example, use clickhouse://appuser:apppass@127.0.0.1:19006/appdb instead of the database’s direct address.

One Sidecar for ClickHouse, PostgreSQL and MySQL

One process runs one lane per upstream, and the lanes share nothing on the wire: a clickhouse lane speaks native ClickHouse, a postgres lane pgwire, a mysql lane the MySQL protocol. What they share is the top of the file. pii, guardrails, mask and analyzer at the top level are defaults every lane inherits, so a rule written once protects the warehouse and both transactional databases. Each lane adds or overrides only what differs.
config.yaml

What each lane resolves to

The file does not show the merge; the running process does. Inheritance follows one rule per block, and the same rule on every protocol:
GET /config on the admin listener returns the same resolved stack by rule name. With provider: vertex, --validate also mints one token, so a bad credential fails here rather than on the first risky statement.

The same rule, three protocols

The classifier reads each dialect with its own lexer, so one rule set means one thing on every lane: A table rule matches a schema-qualified name by its last segment, so tables: [customers] covers appdb.customers on ClickHouse and public.customers on PostgreSQL alike. ClickHouse mutations are ALTER statements: a rule or trigger that names only delete misses ALTER TABLE … DELETE, which is why the warehouse analyzer triggers on alter too. The controls surface in each client’s native error and result framing:

Connect to each lane

Every lane writes to the one audit stream, keyed by connection, so a session that touched all three databases reads as three connections with one principal:
Roll the analyzer out with every tier on warn and read by_risk in /api/stats before moving high to block. The guardrails deny for free and never leave the process; keep every risk you can name in an operation or table rule and spend the model only on what those cannot express. See AI Analyzer Reference.

What the lane reads

The codec follows one connection in both directions because a Query packet decides how its reply is framed. The ClickHouse lexer understands backtick identifiers, # and -- comments, backslash string escapes and double-quoted identifiers. A statement is classified before its Query packet is sent upstream. Multi-statement text is split and every statement is evaluated, even though a native ClickHouse server normally rejects multi-statements itself. The lane is a byte relay, not a ClickHouse account or connection pool. The client still authenticates directly to ClickHouse, and ClickHouse remains the authority for database permissions.

Protocol revision

ClickHouse native packets have no outer length. Their layout depends on the protocol revision negotiated in the two Hello packets. Reading one packet with the wrong layout would also lose the boundary of every packet after it. The lane rewrites both advertised revisions to at most 54450 before either Hello is decoded or forwarded. ClickHouse already negotiates the lower peer revision, so clients and servers use their normal backward-compatibility path. The rewrite is transparent to ordinary queries, but features introduced after the pin are unavailable on this connection.
The lane fails closed on an unknown packet or a flow outside the implemented ordinary query path. It never forwards a stream after it can no longer prove where the next Query packet begins.

Compression and memory limits

LZ4 is ClickHouse’s default native compression and is supported. Uncompressed blocks are supported too. ZSTD is refused because the lane does not inflate it. If a client selects ZSTD, keep native compression enabled and select LZ4:
Every compressed frame carries a CityHash checksum and declared compressed and decompressed sizes. The lane verifies the checksum and checks both sizes before allocating the decompressed buffer. A result set can contain any number of blocks. The lane holds and reuses memory for one block at a time; it does not accumulate the result. The generic stream reassembly limit is derived from max_block_bytes with bounded compression and packet-header slack, so a valid block larger than 8 MiB is not rejected by the generic inspector first. Set limits from the largest single block, not from the total result size. ClickHouse commonly emits about 1 MiB compression frames and up to 65,000 rows per block. Keep the defaults unless a measured query logs a limit refusal, then raise the affected limit only enough for that block shape.
The limits are per active connection. Raising them raises the amount of memory a client can make the Sidecar retain. A frame declaration above max_frame_bytes closes the session before allocation; max_block_bytes closes it as the decompressed frames accumulated for that block cross the cap.

Masking result columns

ClickHouse sends typed columns rather than row strings. The lane decodes a complete block, masks each supported string value, re-encodes the block and rebuilds its LZ4 frames and checksums. A replacement may be longer or shorter without corrupting the client stream. Prefer column rules for columns whose names are stable. Entity rules are useful when the query shape changes or the same sensitive value appears in several columns:
config.yaml
When masking is disabled, supported ordinary query blocks are inspected and forwarded without rebuilding unchanged values.

Denials

A matching guardrail stops the Query packet before it reaches ClickHouse and returns a native ClickHouse Exception with code 497 and the rule’s message:
The audit trail records a violation with the statement, operation, tables, rule and message. Allowed queries produce a client-side statement event and a server-side completion event at Exception or EndOfStream. A rewritten result also emits masked with entity names and a count, never the original values.

TLS on each leg

ClickHouse native TLS starts with a TLS ClientHello on the first byte. The two legs are independent: To encrypt both legs:
config.yaml
Connect to a TLS-enabled client leg with --secure:
upstream_tls does not enable ClickHouse’s secure port. Configure tcp_port_secure and the server certificate in ClickHouse first, then point upstream at that port. Certificate verification or handshake failure closes the connection; the lane does not downgrade to plaintext.

Native, HTTP or an emulation port

One ClickHouse server can expose several protocols. They are separate Sidecar listeners and do not share capabilities. Do not point a clickhouse listener at the HTTP or emulation ports. The protocol field selects the wire decoder; a mismatch fails the connection. The HTTP interface behind an http lane. ClickHouse answers every request Transfer-Encoding: chunked, and the codec re-chunks what it rewrites. FORMAT TSV and CSV rows are lines and go out line by line; JSONEachRow is NDJSON and goes out row by row, each value handed to the masker under its column name, so columns: [email] names the column the way it does on the native lane; FORMAT JSON is one document with the rows under data; RowBinary and Native are binary and pass untouched. With enable_http_compression=1, gzip and deflate are undone around the masker; a client that asks for zstd, br or lz4 on a text or JSON result is answered 403 and the trail gets an error row, because that response would be the one that left unmasked. Guardrails read the request line, so a SQL rule never sees the POST body; ?query= and ?param_x= on a GET are on the line and are scanned. Set http.capture_body: true to record the SQL in the audit trail; it records the response as the server sent it too, so the trail holds the unmasked result set even when the client received it masked.

Troubleshooting

Inspect the resolved listener and its audit events:

Next

Guardrails

Deny ClickHouse statements by operation, table, text pattern or sensitive request values.

Data Masking

Choose entity, column and replacement strategies for native result values.

Config File Reference

Listener inheritance, TLS fields, audit and startup validation.

Sidecar Architecture

Follow one connection through inspection, policy, audit and response rewriting.