Skip to content

Add cluster interserver-secret authentication#1

Open
BorisTyshkevich wants to merge 2 commits intomainfrom
feature/interserver-secret
Open

Add cluster interserver-secret authentication#1
BorisTyshkevich wants to merge 2 commits intomainfrom
feature/interserver-secret

Conversation

@BorisTyshkevich
Copy link
Copy Markdown
Collaborator

Summary

Teaches the driver how to authenticate as a trusted cluster peer using ClickHouse's shared interserver secret, so callers can execute queries as an arbitrary initial_user without a per-user password. This is the same protocol ClickHouse servers use between cluster nodes for distributed queries.

  • Options.Cluster{Name, Secret} turns interserver mode on
  • WithInitialUser(user) sets the impersonated user per query (falls back to Auth.Username)
  • Hello sends " INTERSERVER SECRET " + empty password + cluster name + 32-byte random salt
  • Per-query slot carries SHA256(salt + secret + body + id + initial_user) and ClientQueryInitial is flipped to ClientQuerySecondary
  • Empty-secret path is unchanged — the slot stays the legacy empty string

Driver advertises protocol 54460, so nonce + external-roles additions from DBMS_MIN_REVISION_WITH_INTERSERVER_SECRET_V2 (54462) are intentionally omitted. Matches src/Server/TCPHandler.cpp::processQuery interserver branch.

Test plan

  • go test -count=1 ./lib/proto/ -run Interserver — new query_interserver_test.go covers empty-secret → empty slot, hash layout matches ClickHouse, hash differs per user
  • End-to-end test in a downstream consumer (Altinity/altinity-mcp #): spins up clickhouse/clickhouse-server with a cluster secret, connects with Cluster.Secret set, and verifies SELECT currentUser() returns the impersonated user and that a wrong secret is rejected

Notes for reviewers

  • No changes to the non-cluster code paths — Cluster.Secret == "" is a short-circuit everywhere
  • The hash is string(h.Sum(nil)) (raw 32 bytes) to match ClickHouse's encodeSHA256 which returns binary, not hex
  • This is a fork of ClickHouse/clickhouse-go (main HEAD d280a2a at branch time); upstream PR is a follow-up if desired

🤖 Generated with Claude Code

Allows a client to authenticate as a trusted ClickHouse cluster peer by
sharing the cluster secret instead of a user password, and to execute each
query as an arbitrary `initial_user` which the server accepts via
`AlwaysAllowCredentials`. This mirrors the existing distributed-query
protocol documented in `src/Server/TCPHandler.cpp`.

- `Options.Cluster{Name, Secret}` configures interserver mode.
- `WithInitialUser` sets the impersonated user per query (falls back to
  `Auth.Username`).
- Hello sends `" INTERSERVER SECRET "`, empty password, cluster name, and
  a 32-byte random salt when `Cluster.Secret` is set.
- Each query encodes `SHA256(salt + secret + body + id + initial_user)`
  into the interserver-secret slot and flips `ClientQueryInitial` to
  `ClientQuerySecondary`. When `Cluster.Secret` is empty the slot stays
  the legacy empty string, so existing callers are unaffected.
- Driver advertises protocol 54460, so nonce and externally-granted-roles
  wire additions from `DBMS_MIN_REVISION_WITH_INTERSERVER_SECRET_V2`
  (54462) and later are intentionally omitted.

Unit tests in `lib/proto/query_interserver_test.go` cover the hash layout
and verify the empty-secret branch preserves the legacy empty slot.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
BorisTyshkevich added a commit to Altinity/altinity-mcp that referenced this pull request Apr 20, 2026
Stops requiring a shared ClickHouse username/password for altinity-mcp's
gated deployments. When `clickhouse-cluster-secret` is set, altinity-mcp
handshakes with ClickHouse as a trusted cluster peer using the shared
secret and executes each query as the MCP-authenticated user (taken from
OAuth claims or the configured `clickhouse-username`).

- Add `ClusterName`/`ClusterSecret` to `ClickHouseConfig` with matching
  CLI flags (`--clickhouse-cluster-name`, `--clickhouse-cluster-secret`)
  and `CLICKHOUSE_CLUSTER_*` env vars.
- Reject invalid combinations early: interserver auth is TCP-only and
  requires a non-empty cluster name.
- Wire `ClusterCredentials` through `clickhouse.Open` and drop the
  static password when the secret is set, so only the secret reaches
  the wire.
- In OAuth gating mode, override `chConfig.Username` with the OAuth
  `Subject` when the cluster secret is active so `system.query_log`
  attributes the query to the end user.

Requires the Altinity/clickhouse-go fork which implements the protocol
extension (see Altinity/clickhouse-go#1). The `go.mod` uses a local-path
replace during development — see the in-file comment for the follow-up
resolution path.

Adds `pkg/clickhouse/cluster_secret_test.go` which spins up
`clickhouse/clickhouse-server` with a `<remote_servers>` cluster and
passwordless users (`alice`, `bob`), then verifies `SELECT currentUser()`
returns the impersonated user, that switching Username switches the
effective user, and that a wrong secret is rejected.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant