Helicone 支持用户直接对共享 ClickHouse 写 SQL
速览
Helicone 宣布用户现在可以直接编写 SQL 语句并发送至共享的 ClickHouse 实例。此举可简化数据分析流程,让用户无需额外工具即可实时查询性能数据。Helicone 作为 AI API 追踪平台,通过该功能进一步增强了数据可观测性。
AI 深度解读
背景
Helicone 是一个 AI/API 观测平台,用于监控和分析 LLM 调用等请求数据。其底层存储使用 ClickHouse 集群,所有组织(org)的请求数据均存放在同一张表 request_response_rmt 中,仅通过 organization_id 列区分。为了提供更灵活的数据查询能力,Helicone 计划让用户直接对 ClickHouse 运行 SQL 查询。然而,让不同组织的用户对同一张共享表执行任意 SQL 语句存在严重的安全隐患——必须确保组织 A 无法读取组织 B 的数据。为此,Helicone 设计了一套基于 ClickHouse 内置特性(行级策略与自定义设置)的租户隔离方案,并实现了相应的应用层防护。
核心内容
Helicone 添加了一个名为 HQL 的 SQL 编辑器,用户可以在其中输入 SELECT 语句,查询直接在其 ClickHouse 集群上执行。为了实现安全的租户隔离,他们采用了以下方案:
1. 行级策略(Row Policy)
ClickHouse 的行级策略(Row Policy)允许为特定用户(或角色)在指定表上附加一个过滤条件,该条件会在每次 SELECT 查询时自动追加。Helicone 的迁移脚本中执行了以下两步:
- 创建一个专用用户
hql_user,仅用于 HQL 查询流量。 - 为
request_response_rmt表创建行策略,使用USING organization_id = getSetting('SQL_helicone_organization_id'),并绑定到hql_user。
这样,任何通过 hql_user 发起的 SELECT 查询,都会自动在 WHERE 子句后追加 organization_id = getSetting('SQL_helicone_organization_id'),无需用户自己编写。Helicone 的 ingest 和仪表盘代码使用其他用户连接,不受该策略影响。
2. 自定义设置(Custom Settings)与 readonly 保护
getSetting 从当前查询的上下文中读取一个设置值。ClickHouse 仅识别以声明前缀开头的自定义设置。在自托管环境下,管理员可以在配置文件中指定 custom_settings_prefixes。但在 ClickHouse Cloud 上,无法修改服务器配置,所有自定义设置必须以前缀 SQL_ 开头。因此 Helicone 的设置名为 SQL_helicone_organization_id(虽然名字不优雅,但前缀是强制要求)。
设置值通过 HTTP 查询参数传递:?SQL_helicone_organization_id=<org>。在 Node.js 客户端中,通过 clickhouse_settings 对象传入,其中包括 SQL_helicone_organization_id、readonly: "1"、allow_ddl: 0、max_execution_time: 30、max_memory_usage: "4000000000" 和 max_result_rows: "10000"。
readonly: 1 是关键保护:它阻止用户在查询中的 SETTINGS 子句覆盖 SQL_helicone_organization_id。例如,用户无法通过 SELECT * FROM request_response_rmt SETTINGS SQL_helicone_organization_id = 'victim-org' 来篡改租户 ID。此外,应用层还通过正则表达式拒绝任何包含 sql_helicone_organization_id 的查询,作为额外防御。
3. 权限最小化:撤销所有其他权限
行策略仅保护一张表,但 ClickHouse 的系统表(如 system.query_log)包含所有查询历史,包括 SQL 文本。因此,必须将 hql_user 的权限限制到最低:
REVOKE ALL ON system.* FROM hql_user;REVOKE ALL ON information_schema.* FROM hql_user;REVOKE ALL ON default.* FROM hql_user;GRANT SELECT ON default.request_response_rmt TO hql_user;
这样,用户只能查询 request_response_rmt 表,无法访问其他表或系统信息。此外,为了在编辑器的自动补全中隐藏 organization_id 列,Helicone 在 DESCRIBE 结果中过滤掉了该列,从视觉上掩盖了表是共享的事实。
4. AST 解析器的用途:非安全边界,仅用于 LIMIT 限制
Helicone 故意不在应用层实现租户隔离(例如通过解析 SQL 并自动插入 WHERE organization_id = ?),因为这会引入复杂的解析器兼容性问题。ClickHouse 的语法包含许多特殊构造(如 properties['user_id'] 的 map 下标、arrayJoin 等),大多数 JavaScript SQL 解析器无法正确处理。如果解析器出错,可能导致查询泄漏。
因此,Helicone 仅使用 node-sql-parser(以 Postgres 方言解析)来处理 LIMIT 强制插入和上限控制,而非安全过滤。当解析失败时,会回退到正则表达式修正 LIMIT。由于租户过滤完全由数据库层保证,解析器错误最多导致查询返回更多行,而 max_result_rows 会进一步限制。
5. 子查询的恐惧与数据库级强制
Helicone 团队在设计时最担心的是子查询场景。如果租户隔离依赖应用层重写器,那么 SELECT * FROM (SELECT * FROM request_response_rmt) AS sub 这样的查询只需在外层添加 WHERE organization_id = ? 是无效的,因为内层 SELECT 已经读取了整张表。正确的重写器必须遍历所有嵌套 SELECT、CTE、UNION 分支和 JOIN 操作数,并证明每个都被过滤了。
由于 Helicone 将租户过滤放在数据库层(行策略自动追加到所有读操作),子查询带来的复杂性被完美规避。行策略对任何表引用都生效,无需应用层处理。
6. 测试套件:直接攻击数据库而非解析器
Helicone 的测试套件 hqlSecurityTests.test.ts 长达 843 行,包含大量对抗性查询,直接对带有真实行策略的 ClickHouse 实例运行,以确保数据库强制隔离不出问题。这比依赖解析器正确性更可靠。
关键要点
- 行策略(Row Policy):ClickHouse 的
CREATE ROW POLICY允许为指定用户自动附加过滤条件,是实现租户隔离的核心机制。 - 自定义设置(Custom Settings):通过
getSetting读取查询上下文中的租户 ID,该 ID 通过 HTTP 参数传入,由应用层认证决定,用户不可篡改。 readonly保护:设置readonly: 1可以阻止用户通过SETTINGS子句覆盖关键设置,是防御篡改的关键。- 权限最小化:
hql_user仅拥有SELECT权限于一张表,所有其他权限(包括系统表)被撤销,防止信息泄露。 - 应用层不做安全边界:AST 解析仅用于插入 LIMIT 等非安全操作,租户过滤完全依赖数据库,避免解析器缺陷导致漏洞。
- 子查询安全由数据库自动保障:行策略对任意层级的子查询均生效,应用层无需处理嵌套复杂性。
- 测试直接针对数据库:使用对抗性查询测试真实行策略,确保隔离机制可靠,而非模拟解析器行为。
意义与影响
Helicone 的实践展示了如何安全地让用户直接查询共享 ClickHouse 表,而无需引入复杂的应用层重写或专用中间件。其关键思路是将安全边界下移至数据库层,利用 ClickHouse 原生但常被忽视的行策略和自定义设置功能,配合严格的权限控制和只读保护,实现了简洁且健壮的租户隔离。
这一
