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
listen address:
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: aclickhouse 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
connection, so a
session that touched all three databases reads as three connections with one
principal:
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 most54450 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.
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:
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.
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
Denials
A matching guardrail stops the Query packet before it reaches ClickHouse and returns a native ClickHouse Exception with code497 and the rule’s message:
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
--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.