把生产库直连给大模型查数据,五分钟搞定;想把权限收回来
information_schema 翻了个底朝天,触发了云厂商的「异常连接熔断」,主库只读副本挂了 40 分钟。事后复盘最讽刺的是——撤销那个账号的权限,比申请权限时多跑了六个审批流,还得 DBA 手动 REVOKE、清连接池、改 Terraform、轮换 Secret,前后耗时 36 小时。一、权限收不回,本质是「临时」变成了「永久」
大多数团队给 LLM 配数据库账号,图省事全走「超级只读」或干脆 db_owner。理由总是一样的:「反正只是让它看 Schema、跑 SELECT,又不改数据。」
但 LLM 不懂业务边界,它会把 pg_stat_activity、慢查询视图、甚至 pg_catalog.pg_authid 当成上下文塞进 Prompt。更坑的是连接池复用:PgBouncer 里那条闲置连接可能还握着上一个 Session 的事务快照,REVOKE 执行完老连接照样能查,必须 pg_terminate_backend 逐个杀 PID。上次我就在 pg_stat_activity 里看着十几个 idle in transaction 的幽灵进程,一个个 kill -9 才算彻底断干净。
二、审计链路一旦断裂,事后甩锅无从查起
把凭证塞进环境变量、.env、甚至直接写死在 Python 脚本里的团队不在少数。LLM 请求走的是应用层连接池,数据库层面只看到「应用服务器 IP + 共用账号」,根本分不清哪条 SELECT * FROM users 是真人发的、哪条是 Agent 幻觉出来的。
想查责?只能去翻应用层的请求日志、对齐时间戳、再跟模型调用链路做 Join。上个月有个案子:数据分析师用 LangChain 跑报表,模型把 LIMIT 100 幻觉成 LIMIT 1000000,把从库拖垮。事后追责花了两天,最后只能靠「那个时间段只有他调用过 API」定性。
三、Row Level Security(RLS)才是正解,但没人想配
PostgreSQL 15+ 的 RLS 策略配合 SET ROLE / SET SESSION AUTHORIZATION,能在连接池层面把租户隔离死。可现实是:
- ORM(SQLAlchemy / Prisma / Django)对
SET ROLE支持极差,得自己封装中间件 - 连接池复用时
DISCARD ALL会清掉 Session 变量,RLS 策略瞬间失效 - 多数 DBA 根本不信任「应用层控权限」,坚持要在数据库层建专用账号
结果就是:要么不用 RLS 裸奔,要么每个微服务、每个 Agent 单独建账号、单独配 Pool、单独走 Vault 动态凭证——运维成本指数级上升,团队照样选择「共用只读账号,出事再改」。
四、我现在的最小可行性方案
不追求完美,只求出事能止损、能追责:
# 1. Vault 动态凭证 + 短 TTL(15min)
database:
creds_path: "database/creds/llm-readonly"
ttl: "15m"
max_ttl: "1h"
# 2. 连接池强制 DISCARD ALL + 验证查询
pgbouncer:
pool_mode: transaction
server_reset_query: "DISCARD ALL; SET ROLE llm_guest;"
admin_users: "pgbouncer_admin"
# 3. 仅开放 information_schema + 业务 Schema,系统表全拒
-- migration: 2024_06_10_llm_guest_grants.sql
REVOKE ALL ON ALL TABLES IN SCHEMA pg_catalog FROM llm_guest;
GRANT USAGE ON SCHEMA information_schema TO llm_guest;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO llm_guest;
# 4. 应用层注入 Request-ID 进 Session 变量,审计日志可溯源
SET app.request_id = 'req_abc123';
SET app.actor = 'langchain-agent-v3';关键点:
- 凭证 15 分钟自动过期,不用人工去
REVOKE,Vault 自动轮换 DISCARD ALL配合SET ROLE,把连接池复用时的脏状态彻底清零- 只给
information_schema+ 业务 Schema,系统表、统计视图、权限表一律不可见 - 应用层强制写入
app.request_id,数据库审计日志里直接能看到是哪个 Agent、哪次调用、哪个用户发起的
五、别信「只读就安全」的鬼话
SELECT pg_sleep(10000); 照样能把库拖死;SELECT * FROM pg_largeobject; 照样能把磁盘读爆;SET enable_seqscan = off; 照样能让优化器发疯。
只有把权限收得比「最小权限」还小——连 information_schema.tables 都不给看、只给特定 View、配上 statement_timeout = 5s、配上 RLS 按 request_id 限行——才算把「收回权限」这事儿在架构层面