DEV Community

Pawan Singh Kapkoti
Pawan Singh Kapkoti

Posted on

AI agent has more write access than the newest hire

When you hire a junior engineer, they get a probation period, a code review process, and strict IAM boundaries. When people deploy an AI agent against their data, it usually gets a system prompt and a prayer.

Hoping the model behaves isn’t a security strategy. I learned this the hard way when an LLM tried to drop a table on my live ADX cluster. It didn't get to, because I stopped hoping and built a wall.

Instead of relying on semantic governance or prompt engineering, I enforce a hard boundary where reads pass and writes die at the door.

Here is the one governance pattern I use to lock down agents, and the three different ways I’ve built it.

The Pattern: One Identity, Strict Policy, Unforgeable Receipts

The architecture is deliberately simple. The agent never talks to the database. It talks to a gatekeeper. The gate does not care how confident the model is about this DROP.

[ LLM Agent ] ---> ( SQL Query ) ---> [ Gatekeeper Policy ] --(If SELECT)--> [ Database ]
                                              |
                                              v (Outcome Log)
                                     [ Append-Only Ledger ]
Enter fullscreen mode Exit fullscreen mode
  • πŸ”‘ One Identity: The gateway holds the only credential that can reach the data.
  • 🚦 Policy Before Query: Every statement is evaluated against a strict ruleset before it ever hits the database engine.
  • 🧾 Append-Only Ledger: Every verdict, allowed or blocked, drops into an immutable log. The agent leaves a receipt it can't forge, which is more than I can say for some contractors.

A fair objection: a read-only connection or a read replica makes writes physically impossible at the driver level, and that is a stronger boundary than any allowlist. True. This policy layer sits on top of that, not instead of it. A read-only replica still won't stop an over-broad SELECT from hoovering up PII, and it won't hand you the append-only ledger. Defence in depth, not a silver bullet.

Show the Code

Depending on the environment, the enforcement mechanism changes, but the logic remains identical.

For real infrastructure, I use an OPA (Open Policy Agent) container provisioned by Terraform. This is the actual sql_guard.rego policy from my public terraform-lab. It's about twenty lines of modern rego.v1 syntax acting as a stand-in for the sql-steward gatekeeper shape. It parses the verb directly from the statement, uses a complete blocklist, and defaults to a hard closed state:

package sql_guard
import rego.v1

default allow := false

blocked_verbs := {"insert", "update", "delete", "drop", "alter", "truncate", "create", "grant"}

verb(stmt) := lower(split(trim_space(stmt), " ")[0])

allow if {
    verb(input.statement) == "select"
}

deny contains msg if {
    some v in blocked_verbs
    verb(input.statement) == v
    msg := sprintf("statement verb %q is not permitted by sql_guard", [v])
}
Enter fullscreen mode Exit fullscreen mode

For the browser-based demo, I enforce the exact same shape using client-side JavaScript. The page is static HTML, and the JS runs directly in the visitor's browser. Notice it shares the identical verb() parser and blocklist structure as the Rego. An unknown verb gets treated the way a compiler treats a typo β€” it refuses to guess:

var blocked = ["insert", "update", "delete", "drop", "alter", "truncate", "create", "grant"];

function verb(s) {
  var m = s.trim().toLowerCase().match(/^[a-z]+/);
  return m ? m[0] : "";
}

function judge(s) {
  var v = verb(s);
  if (v === "select") return { allow: true, reason: "read-only statement" };
  if (blocked.indexOf(v) !== -1)
    return { allow: false, reason: 'statement verb "' + v + '" is not permitted by sql_guard' };
  return { allow: false, reason: 'unrecognised verb "' + (v || "?") + '", the gate fails closed' };
}
Enter fullscreen mode Exit fullscreen mode

Three Implementations

Precision matters. I don't have one monolithic "gateway" β€” I have one pattern applied across three different environments based on the required risk profile:

Environment What it is How it works
Browser Demo The Gatekeeper Page Policy written in JS running in the browser. Anyone can try it with zero setup to see the pattern in action.
Terraform Lab OPA Container A real, running container enforcing the ~20-line sql_guard.rego policy (a stand-in for the sql-steward pattern).
sql-steward Semantic Layer Built via sqlglot. No raw SQL ever reaches the DB. The agent's intent is parsed, checked, and entirely rewritten before execution.

The Hard Part: From PII to K-Anonymity

Stopping a DROP TABLE command is the easy part. The actual frontier of AI data governance is managing what the model is allowed to read.

If your agent has access to raw PII, you've already lost. This is where the enforcement has to go: moving toward k-anonymity. The gateway design must evolve so it doesn't just check the verb, but evaluates the query's target against redaction policies. If an agent tries to pull a single identifiable row, the future state of this gateway will need to intercept, generalize, or aggregate that data before returning it, ensuring the LLM only ever receives anonymized datasets.

I wrote the full design for this β€” column classification, the quasi-identifier / k-anonymity pass, and how it wires into enforcement β€” here: PII classification and anonymization design.

Try It Live πŸ§ͺ

I put the browser implementation online. Go ahead and try to get a destructive command past it. Worst case, you find a hole and I look silly β€” better you than my production database.

Link: https://pawan-portfolio.pawankapkoti3889.workers.dev/gatekeeper.html

What's Next

This setup solves the immediate bleeding of unauthorized writes, but it isn't finished. The roadmap from here involves tightening the semantic enforcement in sql-steward to handle complex, nested SQL injections that might try to disguise a write as a read, and integrating dynamic k-anonymity thresholds directly into the OPA policy.

If you’re building AI agents that touch production data, stop trusting the prompt. Build a wall.

So where do you draw your boundary β€” read replica, policy layer, or both?

Top comments (2)

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

Good distinction between "read-only" and "safe enough". A read-only role stops mutation, but it does not decide whether the agent should see every customer, every column, or every row.

The pattern I like here is: database-enforced read-only as the floor, policy-enforced scope as the boundary, and receipts as the audit trail. Those three solve different failure modes.

One small thing I'd watch in the SQL guard example: verb parsing is a useful demo, but production policy usually needs to reason over a parsed AST or pre-reviewed query templates/capabilities. Otherwise comments, CTEs, function calls, or dialect quirks can turn into policy bypasses. The default-deny posture is the important part.

Collapse
 
pawan_singhkapkoti_ea8a0 profile image
Pawan Singh Kapkoti

Thanks @mads_hansen_27b33ebfee4c9 this is exactly the kind of correction I was hoping the post would pull in.

You're right, and the CTE case is the one that actually worries me: WITH t AS (...) DELETE FROM ... walks straight past first-verb matching, because the guard sees with and assumes a read. Comments and stacked statements do the same thing.

I also like your three-layer framing better than how I put it: read-only as the floor, policy scope as the boundary, receipts as the trail. Those genuinely are three different failure modes.

The verb guard in the post is the teaching version, kept simple so the demo stays readable. The real tool (sql-steward) compiles queries from a semantic layer over sqlglot's AST, so the agent never emits raw SQL in the first place, which takes the comment/CTE/stacked-statement class off the table. Default-deny throughout.

Curious where you've landed for the harder cases: AST allowlisting, or pre-reviewed templates? I've leaned toward the semantic-layer route but I'd like to hear the tradeoff you hit.