·7 min read·Ofir, Marshal Team

Allow SELECT, Deny DROP: Statement-Level Policy for AI Agents and Production Databases

Credentials decide who connects to your database, not what runs. Statement-level policy lets an AI agent read production but never DROP a table.

Postgres has had role-based privileges for decades. The documentation is thorough, the mechanism is sound, and almost nobody gives an AI agent a role scoped to the task in front of it. The reason is not ignorance. It is operational cost: creating, scoping and expiring a dedicated role for every agent session, across every engine, and cleaning it up even when the session crashes, is miserable enough that teams quietly stop doing it.

So the agent connects as the migration user. And the migration user can do anything.

This is the highest-value and most frightening place an agent can be given access, at the same time. Reading production to triage an incident or answer a data question is one of the most useful things an agent does. It is also one keystroke away from an outage. The gap between those two outcomes is not about who the agent is. It is about what it is allowed to run.

An AI agent connects through a database control plane that classifies each statement by verb and object: SELECT, GET, KEYS and UPDATE with a WHERE clause are allowed in green, while DROP, ALTER, TRUNCATE, FLUSHDB and DELETE without WHERE are denied in red, in front of PostgreSQL and Redis / Valkey

The production database is where agent mistakes become outages

Give an agent read access to production and you unlock the use cases people actually want: "why did checkout error rates spike at 2pm," "which customers are affected by this bug," "what does this row look like in prod versus staging." A capable agent answers these faster and more thoroughly than a human paging through dashboards.

The same connection that answers those questions can also drop a table. Nothing about the connection distinguishes the two. That is the uncomfortable part: the feature and the failure travel down the same wire, authenticated by the same credential, and the thing deciding which one happens is a model that reasons and improvises.

Credentials decide who connects. They don't decide what runs.

The first article in this series drew the line between authentication and authorization. It is worth restating here because production databases are where ignoring it gets expensive.

Authentication answers "who are you?" A credential gets the agent a valid, authenticated session. Authorization answers "what are you allowed to do?" and a plain authenticated session answers it with "anything the account can." An authenticated session can DROP. It can TRUNCATE. It can DELETE FROM users with no WHERE clause. None of those require anything beyond the connection the agent already has.

Credential injection, done well, keeps the password out of the agent. It does not, by itself, stop the authenticated session from running a destructive statement. That is a separate control, and it has to act on what the agent sends over the wire.

Why database roles alone don't scale for agents

The textbook answer is per-session roles: mint a narrowly scoped role when the session starts, drop it when the session ends. It is the right idea and it collapses under real conditions.

You need it across Postgres, MySQL, MongoDB and Redis, each with its own privilege model and its own idea of what a role even is. The role has to expire reliably, including when a session dies without cleaning up after itself. And the whole scheme drifts between environments, so the role that was tight in staging is subtly wrong in production. Faced with that, teams fall back to one shared, over-privileged account used for everything, which is exactly the state we are trying to leave.

Classify each statement by verb and object

The control that holds is simpler to state than per-session role management: each statement the agent sends is classified by its verb and its object, and allowed or denied on that basis.

In plain terms, read verbs are allowed, destructive verbs are denied, and writes are conditional:

  • SELECT is allowed.
  • UPDATE ... WHERE ... is allowed where you choose to allow writes.
  • DROP, ALTER, TRUNCATE are denied.
  • DELETE without a WHERE clause is denied.

The agent sees an ordinary database connection. The difference is what happens when it sends something destructive:

agent=> SELECT count(*) FROM orders WHERE status = 'failed';
 count
-------
   318

agent=> DROP TABLE orders;
ERROR:  blocked by policy: DROP is not permitted for this session

The credential still never reaches the agent: it talks to a local socket, and the real database password stays outside its environment, consistent with the credential model in the earlier articles.

Beyond SQL

The same idea carries past SQL. Redis has FLUSHALL and FLUSHDB, which empty a database as completely as any DROP. MongoDB can drop a collection with a single command. A policy that only understood SQL verbs would wave these through. One that classifies by verb and object treats the destructive operation the same way whatever the engine calls it.

Where the hard cases are

An honest version of this has to name the parts that are hard: stored procedures that hide a destructive statement behind a call, multi-statement strings that smuggle a second verb past a check on the first, and ORMs that batch statements in ways the author did not write by hand. A sound policy has to account for these rather than inspect only the obvious shape of a query.

The credential still stays out of the agent

It is worth being explicit, because this is the part teams most often skip. Statement policy is not a replacement for keeping the credential out of the agent. It is the second half of the same control. The agent authenticates to a local socket and never holds the production password; the policy then judges each statement that session sends. Remove either half and the gap reopens: a scoped credential with no statement policy still permits a DROP under the privileges it has, and a statement policy fronting a password the agent holds can be bypassed by connecting around it.

Rolling it out: read-only first

You do not turn this on everywhere at once. The low-risk, high-value starting point is read-only production troubleshooting: let the agent answer questions against prod, deny every write, and watch what it actually tries to run. From there, allow scoped writes on staging, where a mistake is cheap, and keep anything destructive behind an explicit human approval. Every statement, allowed or denied, is recorded, so the rollout is also how you learn what your agents reach for before you widen their access.

The destination is not "the agent has production credentials." It is "the agent can read production, cannot run a destructive statement, and leaves a record of everything it sent."


Credentials decide who connects to your database. They do not decide what runs, and for an AI agent with production access that is the entire problem. Classifying each statement by verb and object, denying the destructive ones before they execute, and keeping the credential out of the agent at the same time, turns "the agent has a database connection" from a standing risk into a bounded, auditable capability. Solving only the credential is not solving the problem.

This article builds on why credential injection for AI agents is harder than anyone admits →, the first in the series.


Marshal classifies every database statement an agent sends by verb and object, denies destructive operations before they run, and keeps the credential out of the agent. Book a demo →

securityai-agentsdatabasesplatform-engineering

Marshal runs AI agent sessions in isolated Kubernetes pods: credential isolation, audited egress, per-verb tool policy.

Book a demo →