AI代理数据库安全访问框架

为AI代理提供安全的PostgreSQL数据库访问框架,通过全表行级安全、只读查询限制和白名单写路径,实现可控读取与受限写入,适用于金融工作流、合规检查和内容运营等场景。

  • by MrMinor-dev
  • 本地部署
  • 更新于
在 GitHub 查看 MrMinor-dev/database-security-framework

使用说明

Database Security Framework

How I gave an AI agent database access without giving it the keys: row-level security on every table, read-only queries, and a write path that only touches what I allow.

An agent that runs financial workflows, compliance checks, and content operations has to read broadly and write carefully. It needs SELECT across dozens of tables. It gets INSERT on a handful, UPDATE on a few columns, and DELETE on 3 tables with a WHERE clause.

Most AI-to-database setups pick full access or no access. Full access is dangerous. No access is useless. This is the middle, built on a Supabase Postgres schema of 59 tables.

An early audit found 8 real errors in a system that looked secure from every angle I had checked. 5 tables had no row-level security. 3 views ran with their creator's permissions and skipped RLS. RLS was "enabled" in every design document. It just wasn't enabled on those 5 tables. The fix took minutes. Finding the gaps took a method that checks each table and each view.

Then it happened again, in a smaller way. See the views that skipped RLS.

What's here

File What it is
sql/rls-policies.sql Row-level security state and policies, from the live schema.
sql/rls-audit.sql The audit queries: tables without RLS, views that skip it, what the public roles can reach, default privileges.
config/write-allowlist.json The tables and columns the write service may touch, trimmed to tables that exist today.
workflows/data/aos-database-safe-sql-query.json The read-only query service. Sanitized n8n export.
workflows/data/aos-database-safe-db-write.json The allowlisted write service. Sanitized n8n export. Being rebuilt. See the note below.
LICENSE MIT.

sql/rls-policies.sql leaves out the agency_ tables, which belong to a separate client-facing system. All 59 tables have RLS on.

Note on the write workflow

The Safe DB Write workflow is published as a record of the design. It is being rebuilt. Its final database node, an n8n Postgres node at version 2.5, does not commit writes. The workflow runs, validates, and reports success, and the data doesn't change. I found this in testing and haven't trusted the workflow since. Writes go through the Supabase REST API today, with the same allowlist. The validation code in the export is the design that carries forward. The write step is what's broken.

The export also still lists tables from a retired project. The trimmed list in config/write-allowlist.json matches what exists today.

Architecture

            Agent request
                 |
        Input router
   handles MCP and webhook payloads
                 |
        Operation classifier
         /                \
   Safe SQL query      Safe DB write
   - SELECT only       - table allowlist
   - 9 statement       - column allowlist on UPDATE
     types blocked     - DELETE needs a WHERE
   - 50-row cap          and never a bulk pattern
         \                /
     Postgres + row-level security
   - RLS on every table
   - one service-role policy per table
   - a few named app roles with narrow policies

Input router. n8n receives a request differently depending on how it was called. An MCP call puts the text in chatInput. A webhook puts it in body.query. The validation step checks both before any database step runs.

Safe SQL query (read path). A webhook that runs SELECT and nothing else. It blocks 9 statements: insert, update, delete, drop, alter, truncate, create, grant, revoke. It returns at most 50 rows.

Safe DB write (write path). A webhook with per-table permissions, as configured in the export:

  • INSERT is allowed on 12 tables. There is no column list on INSERT.
  • UPDATE is allowed on 16 tables, each with a list of columns it may change.
  • DELETE is allowed on 5 tables. It needs a WHERE clause and rejects where true and where 1=1.
  • DROP, ALTER, TRUNCATE, CREATE, GRANT and REVOKE are always blocked.
  • Of those tables, 5 for INSERT, 8 for UPDATE and 3 for DELETE exist today. The rest belonged to a retired project.
  • One table used only by a separate business line is omitted from the published config.

Row-level security. RLS is on for all 59 tables. Service-role policies control what the agent reaches. Table prefixes mark who owns each table: aos_ for operations, agency_ for client work, haios_ for the human-AI coordination layer.

What the audit found

The design documents said RLS was enabled and access controls were in place. So I stopped reading documents and checked the database, table by table and view by view. It found 8 errors:

  • 5 tables with no RLS at all
  • 3 views with SECURITY DEFINER, which run with the creator's permissions and not the caller's

These were production gaps in a system that had run for months. "Secured" is a claim. Each table and each view has to prove it.

The views that skipped RLS

A later audit cleared 4 views on their content. They showed names with the email masked and left the raw text out, and I judged them safe because of what they exposed.

I had checked what the views showed and skipped how they ran. Views run with their creator's rights unless you say otherwise, so they skip row-level security. Anyone holding the public API key could read them, and their grants looked broad enough that writes through them were likely too.

On 2026-09-29 I set security_invoker on all 4 and revoked access from the public roles. Then I checked from the outside: the public key now gets permission denied on every one. I also changed the default privileges so new tables created by the main role no longer grant anything to the public roles automatically.

The sql/rls-audit.sql queries now include a check for this: views without security_invoker, and what each public role can actually reach.

The lesson repeats. Review what an object exposes and also how it runs.

The bug between 2 systems

Another find came from intermittent query failures. Sometimes a query worked and sometimes it came back empty. The SQL was right. The logic was right.

The cause was how n8n hands over the request. An MCP call and a webhook call put the query text in different places. The failure only showed when I changed how I called the workflow, and nothing at either layer pointed at it.

The fix is one line that checks both places:

$input.first().json.body?.query || $input.first().json.chatInput || ''

Numbers

Item Value
Production tables 59, with RLS on all 59
Blocked statements on the read path 9
Read-path row limit 50
Write allowlist as configured 12 INSERT, 16 UPDATE, 5 DELETE tables
Write allowlist that exists today 5 INSERT, 8 UPDATE, 3 DELETE tables
Errors found by the early audit 8 (5 missing RLS, 3 SECURITY DEFINER views)

Why this matters for security work

Least privilege for an AI agent is the same problem as least privilege for any account. Which identity gets which resource, at which operation level, under what conditions. Allowlist by table, restrict by column, require a WHERE clause on anything destructive. That is the standard enterprise model, applied to a new kind of user.

The part that carries over is the audit method. Check the actual state of every object and ignore the documented intent. That is where real security failures sit, in the gap between what the architecture says and what the system does.

Built with

Supabase (Postgres and pgvector), row-level security, n8n webhooks, and the Model Context Protocol.


Jordan Waxman · LinkedIn · GitHub profile