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:
- MCP client sends OAuth bearer.
pkg/server/server.go:1077–1086 GetClickHouseClientWithOAuth extracts OAuthClaims.Email (then Subject), stuffs it into chConfig.Username, clears Password.
pkg/clickhouse/client.go:120 NewClient → connect() opens a per-MCP-request clickhouse.Open(...) with Cluster: ClusterCredentials{Name, Secret} + Auth.Username = email.
- The Altinity clickhouse-go fork sends
" INTERSERVER SECRET " in the handshake. Auth.Username becomes the per-query initial_user.
- 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
- 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?
- Row-policy use. Customers relying heavily on row policies will trip #100695. Do we have a sense of how many do?
- 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.
- 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.)
- 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).
- 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?
Why I'm raising this
feature/interserver-auth(commitsdea9214,eb2c479,5c20414,feb0972) gives us per-user query attribution by handshaking with ClickHouse using the cluster's interserver<secret>and settingAuth.Usernameto the OAuth user's email/sub. It works, and it ships. But:clickhouse-go(commit5c20414) 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.<remote_servers>entries). There is no per-impersonation-pair authorization.ClickHouse 25.11 added
EXECUTE AS user, gated byGRANT IMPERSONATE. In 26.3 LTS the feature is enabled by default (PR #97870); on 25.11–26.2 it requires theaccess_control_improvements.allow_impersonate_user=1server 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-authdoes todayEnd-to-end identity flow:
pkg/server/server.go:1077–1086 GetClickHouseClientWithOAuthextractsOAuthClaims.Email(thenSubject), stuffs it intochConfig.Username, clearsPassword.pkg/clickhouse/client.go:120 NewClient → connect()opens a per-MCP-requestclickhouse.Open(...)withCluster: ClusterCredentials{Name, Secret}+Auth.Username = email." INTERSERVER SECRET "in the handshake.Auth.Usernamebecomes the per-queryinitial_user.pkg/server/server.go) callchClient.Query / Exec / Selectwith no per-call user plumbing — identity is baked into the connection.Per-request
Clientis created and torn down at every tool invocation (every handler doesdefer chClient.Close()).What
EXECUTE ASwould look likeReplace the wire-protocol primitive with a SQL primitive, keep the rest of the per-request flow.
mcp_gateway— with a regular password.GRANT IMPERSONATE ON * TO mcp_gatewayonce.EffectiveUserfield on the per-requestClickHouseConfig.Client.executeSelect / executeNonSelect / DescribeTable / ListTablesprependEXECUTE AS '<EffectiveUser>'to the SQL whenEffectiveUseris set. (The two-form scopes EXECUTE AS to a single subquery; no session-state mutation across pooled connections.)system.query_log.usershows the impersonated user, just like today.system.session_logadditionally records themcp_gatewaylogin — audit trail of who held the credential, which we don't have today.Code-level scope is small:
pkg/clickhouse/client.go(dropCluster, add the wrap helper),pkg/server/server.go:1077–1086(setEffectiveUserinstead ofUsername),pkg/config/config.go(drop two fields, add one runtime-only field), revert5c20414to point back at upstreamgithub.com/ClickHouse/clickhouse-go/v2. The cluster-secret integration test file becomes an EXECUTE AS test against the same testcontainers scaffold.Honest comparison
EXECUTE AS<remote_servers><secret>per clusterGRANT IMPERSONATE; on 25.11–26.2 alsoallow_impersonate_user=1mcp_gatewaywith normal credentialsOpen questions for the team
pkg/server/server.go's tool handlers and didn't see explicitFORMATclauses, but user-supplied SQL viaexecute_querymay include them. Worth checking whether #99572 hits us — and if so, whether routing through HTTPdefault_formatheader is acceptable.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 SQLALTER USER).