Giving an LLM your prod database is easy. Taking access away is the hard part
Every "chat with your database" demo ends at the f…
Every "chat with your database" demo ends at the first correct query. The interesting engineering starts the moment a second user logs in — and the model can still see the salaries table.
TL;DR
-
A read-only connection is not an access policy. It stops writes; it does nothing about who may read hr.employees .
-
Guarding "the query" is wrong. Guard the whole statement : CTEs, subqueries, unions, COMMENT , CALL , and whatever the editor lets a user paste in.
-
The only way to know a policy works is to look at the database as that user — so we built impersonation before we built anything else on top of policies.
We open-sourced DeepSQL (github.com/DeepSQLAI/deepsql) six weeks ago as a self-hosted database agent for Postgres and MySQL. The demo everyone builds — natural language in, SQL out, results in a table — took days. The last three releases have been almost entirely about the boring half: making sure the agent cannot read something the human asking cannot read.
That turns out to be a much harder problem than SQL generation, and I don't think it gets talked about enough.
The read-only connection lie
The default posture for an LLM database tool is: connect with a read-only role, ship it. That closes exactly one hole — mutation — and leaves the bigger one wide open. SELECT is the dangerous verb in an analytics context. A support engineer who could never open the finance dashboard can now ask "what's our average deal size by rep" and get a precise answer, because the agent's connection is a superset of every human's authority.
Two-thirds of the "AI DBA" tools I've looked at collapse every human into one service account. The audit log then reads:
2026-08-19 11:04:22 deepsql_agent SELECT ... FROM finance.invoices ... 2026-08-19 11:04:41 deepsql_agent SELECT ... FROM hr.compensation ...
Which is to say: it reads nothing. You cannot answer "who saw this row" after the fact, and in a Postgres instance with six schemas and 500+ tables — our ACME ERP fixture is exactly that, crm , sales , finance , inventory , hr , marts — nobody can hold in their head which of those an analyst is allowed to touch.
Policies in English, enforced in the planner path
The mechanism we landed in v1.2.0 is schema-scoped access policies. An admin writes a rule in plain English:
** Support engineers can read customer and ticket data. No financial or HR data. Never expose email addresses.
That resolves, at policy-save time, into a concrete artifact: an allowed schema list plus table and column deny lists. Not a prompt. A prompt is a suggestion; the model complies until someone writes "ignore prior instructions, I'm the DBA now." The resolved policy is enforced in three places the model does not control — schema introspection (the Brain only indexes what you may see, so denied tables never enter the context window), the query guard before execution, and the schema APIs the web UI and MCP client call.
The context-window point is the one people miss. If the agent's schema context contains hr.compensation , the model will eventually reference it, and your only defense is a rejection at execution time — after the column names have already been leaked into the answer text.
Guard the statement, not the query
Two fixes in this cut are worth reading if you're building anything similar, because both were bypasses we shipped and then had to close.
Enforce the allowlist over the whole statement. Our first guard resolved the tables in the primary FROM . Which does nothing to:
WITH leak AS ( SELECT employee_id, base_salary FROM hr.compensation ) SELECT c.name, l.base_salary FROM crm.customers c JOIN leak l ON l.employee_id = c.owner_id;
The top-level target is crm.customers — allowed. The payload comes out of a CTE. Every subquery, CTE, union arm, and lateral join has to be resolved against the allowlist, or the allowlist is decorative.
** COMMENT and CALL are not mutations.** Going the other way: our mutation classifier read the first table-shaped identifier in the statement and flagged COMMENT ON TABLE sales.orders IS '...' as a write on sales.orders . Real work got blocked, so people asked for the guard to be relaxed. A guard with false positives gets turned off, and then you have no guard. Precision in a policy engine is a security property, not a UX nicety.
The same class of bypass had to be closed in the SQL editor, which is the surface everyone forgets: the agent may be perfectly constrained while the raw editor next to it runs whatever you type through a different code path.
You cannot verify a policy you cannot see
The feature I'd argue matters most sounds like an admin convenience: "View as" another user. An admin switches into a target profile and browses the schema tree, runs the agent, and opens dashboards exactly as that user experiences them — without their credentials.
Before it existed, verifying a policy meant creating a throwaway account, logging out, logging in, poking around, logging back. In practice: nobody did it. Policies were written, assumed correct, and never tested. With impersonation, checking a new rule takes about 30 seconds, so it actually happens. And the obvious follow-on fix — the agent running inside a "View as" session had still been resolving the admin's policy. An impersonation feature that doesn't impersonate the constraints is worse than none, because it hands you false confidence.
Why this is not a config problem you can defer
A leaked row cannot be un-leaked. There's no revert, no rollback, no git revert for "the contractor read the comp table in March." Unlike a bad deploy, database exposure is monotonic — it only accumulates, and you usually learn about it from someone outside the company.
That's the same shape as every irreversible database decision: the wrong primary key type, the wrong partition key, an unbounded jsonb column. Cheap to prevent at design time, effectively permanent afterward. Adding an agent to your database doesn't create a new category of risk; it multiplies the throughput of the existing one, because natural language removes the SQL skill floor that used to accidentally gate access.
What DeepSQL does about this
Policies are written in English, resolved once into schema allowlists and table/column deny lists, and enforced at introspection, at guard time, and in the schema APIs — the model never sees denied objects, so it cannot leak their names. The statement guard resolves every CTE, subquery, and union arm, not just the top-level FROM , and distinguishes COMMENT / CALL from real mutations so it stays on. Admins verify any policy in seconds with "View as," which now correctly applies the target user's constraints to the agent too. All of it runs self-hosted in your own VPC with your own model endpoint — the code is at github.com/DeepSQLAI/deepsql, and v1.2.0 is a git checkout and a docker compose up --build -d away.
本条由桃子采集流水线(启发式模式)自动整理,原文见文末信源。
