Skip to content

Consider EXECUTE AS as alternative to cluster-secret impersonation on feature/interserver-auth #99

@BorisTyshkevich

Description

@BorisTyshkevich

Why I'm raising this

feature/interserver-auth (commits dea9214, eb2c479, 5c20414, feb0972) gives us per-user query attribution by handshaking with ClickHouse using the cluster's interserver <secret> and setting Auth.Username to the OAuth user's email/sub. It works, and it ships. But:

  • It pulls altinity-mcp onto an Altinity fork of clickhouse-go (commit 5c20414) that adds a client-side path through the interserver protocol. We are now responsible for keeping that fork alive against upstream — at minimum tracking, periodically rebasing, and explaining to ClickHouse upstream why we want it.
  • Anyone holding the cluster secret can impersonate any user on the cluster. Scoping is operational only (per-cluster-name <remote_servers> entries). There is no per-impersonation-pair authorization.
  • The secret's blast radius is the whole cluster, and rotation is a cluster-wide event coordinated with every node config and every altinity-mcp deployment that holds it.

ClickHouse 25.11 added EXECUTE AS user, gated by GRANT IMPERSONATE. In 26.3 LTS the feature is enabled by default (PR #97870); on 25.11–26.2 it requires the access_control_improvements.allow_impersonate_user=1 server toggle (relocation PR #96451). It is now the SQL-level primitive that the ClickHouse team intends for cross-user impersonation. Worth deciding deliberately whether we want to switch to it before we invest more in the cluster-secret path.

What feature/interserver-auth does today

End-to-end identity flow:

  1. MCP client sends OAuth bearer.
  2. pkg/server/server.go:1077–1086 GetClickHouseClientWithOAuth extracts OAuthClaims.Email (then Subject), stuffs it into chConfig.Username, clears Password.
  3. pkg/clickhouse/client.go:120 NewClient → connect() opens a per-MCP-request clickhouse.Open(...) with Cluster: ClusterCredentials{Name, Secret} + Auth.Username = email.
  4. The Altinity clickhouse-go fork sends " INTERSERVER SECRET " in the handshake. Auth.Username becomes the per-query initial_user.
  5. Tool handlers (pkg/server/server.go) call chClient.Query / Exec / Select with no per-call user plumbing — identity is baked into the connection.

Per-request Client is created and torn down at every tool invocation (every handler does defer chClient.Close()).

What EXECUTE AS would look like

Replace the wire-protocol primitive with a SQL primitive, keep the rest of the per-request flow.

  • altinity-mcp authenticates as a real, normal SQL user — call it mcp_gateway — with a regular password.
  • Server admin grants GRANT IMPERSONATE ON * TO mcp_gateway once.
  • For every tool call, the OAuth claim's email becomes an EffectiveUser field on the per-request ClickHouseConfig.
  • Client.executeSelect / executeNonSelect / DescribeTable / ListTables prepend EXECUTE AS '<EffectiveUser>' to the SQL when EffectiveUser is set. (The two-form scopes EXECUTE AS to a single subquery; no session-state mutation across pooled connections.)
  • system.query_log.user shows the impersonated user, just like today.
  • system.session_log additionally records the mcp_gateway login — audit trail of who held the credential, which we don't have today.

Code-level scope is small: pkg/clickhouse/client.go (drop Cluster, add the wrap helper), pkg/server/server.go:1077–1086 (set EffectiveUser instead of Username), pkg/config/config.go (drop two fields, add one runtime-only field), revert 5c20414 to point back at upstream github.com/ClickHouse/clickhouse-go/v2. The cluster-secret integration test file becomes an EXECUTE AS test against the same testcontainers scaffold.

Honest comparison

Cluster interserver-secret (today) EXECUTE AS
Server version Stable since 21.6 25.11+ (default on in 26.3 LTS)
Server config <remote_servers><secret> per cluster GRANT IMPERSONATE; on 25.11–26.2 also allow_impersonate_user=1
Authorization grain All-or-nothing per cluster secret Per-target-user grant
Holding identity A 32+ byte shared secret in altinity-mcp memory A SQL user mcp_gateway with normal credentials
Credential rotation Cluster-wide (server configs) Per-app (rotate the gateway user's password)
Driver dependency Altinity fork of clickhouse-go Upstream clickhouse-go
SQL-injection surface None — identity in protocol field Identity goes through SQL text
Known bugs None we've hit #99572 FORMAT clause dropped; #100695 row-policy crash

Open questions for the team

  1. Customer ClickHouse versions. Do we have customers stuck below 25.11? If a meaningful share of deployments is on 24.x LTS, we either keep both paths or wait. What's the actual distribution?
  2. Row-policy use. Customers relying heavily on row policies will trip #100695. Do we have a sense of how many do?
  3. FORMAT in tool-emitted SQL. I grepped pkg/server/server.go's tool handlers and didn't see explicit FORMAT clauses, but user-supplied SQL via execute_query may include them. Worth checking whether #99572 hits us — and if so, whether routing through HTTP default_format header is acceptable.
  4. Driver fork. Is anyone else internally relying on the Altinity clickhouse-go fork's client-side interserver path? If altinity-mcp is the only consumer, dropping back to upstream removes the fork-maintenance bill entirely. (Open question separately: whether to still upstream the cluster-secret feature even if altinity-mcp doesn't use it. PR is at ClickHouse/clickhouse-go #1855.)
  5. Operational story for mcp_gateway. Per-deployment SQL user feels heavier than a shared secret in some senses (more accounts to provision) and lighter in others (rotation is a normal SQL ALTER USER).
  6. Defaults during transition. If we accept the switch, do we deprecate cluster-secret in one release with a warning and remove in the next, or carry both paths indefinitely?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions