Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

AI Agent Permissions: Access Control, Auth and Row Level Security

Most AI agent tutorials spend almost all their time on the model and the tools. Data gets one line near the end: connect the agent to your database. In practice that usually means a connection string with full access, si

AI Agent Permissions: Access Control, Auth and Row Level Security

Most AI agent tutorials spend almost all their time on the model and the tools. Data gets one line near the end: connect the agent to your database. In practice that usually means a connection string with full access, sitting in an environment variable.

For a demo, that's fine. Once real users show up, it means the agent can read any row in any table, and the only thing deciding what it shows is the prompt.

This post covers how permissions for AI agents work, what usually goes wrong, and how to set things up so the database decides what an agent can see. I'll use Nhost for the hands-on examples, but the same approach works on plain Postgres.

The agent calls the backend with the signed-in user's token, so the backend applies that user's permissions

What Are AI Agent Permissions?

AI agent permissions are the rules for what an agent can read, change, and trigger in your app.

Most agents in real products work on behalf of a user: a support assistant, a copilot inside a dashboard, a chat that answers questions about someone's account. For those, the agent should get the same access as the user it's helping.

If Anna can see her own invoices, the agent helping Anna can see Anna's invoices. It can't see Ben's, and it can't do anything Anna couldn't do herself in the UI.

Agents that run without a user behind them, like a nightly job that summarizes support tickets, are a separate case. They get their own narrow role, and we'll come back to them later.

AI Agent Authentication vs Authorization

Authentication answers who sent a request. Authorization answers what that sender is allowed to do. With agents the line gets blurry, because the agent sends the request while a person stands behind it.

AI Agent Authentication Methods

In practice, agents authenticate in one of three ways:

  • With an admin or service key. Full access, set up in a minute. Most demos do this.
  • With their own identity and role. Good for background jobs that don't act for anyone in particular.
  • With the signed-in user's token, passed along on every request. Each call goes out as that user.

For an agent that lives inside your app and helps users, you want the third one.

How AI Agent Authorization Works

Once requests carry the user's token, authorization needs nothing new. The backend sees a request from Anna and applies Anna's rules, exactly as it does when she clicks around the UI.

The agent never has to learn your permission model, and you don't have to describe it in the prompt.

AI Agent Security Risks

Most write-ups on AI agent security focus on the model: jailbreaks, hallucinated actions, bad tool calls. In apps with user data, the more common problem is plainer. The agent can reach data it was never supposed to see.

Admin Keys in the Agent

The agent needs to look up orders, the service key is right there, and it works on the first try. From then on every row in the database is one tool call away, and the model picks which rows to return.

Access Rules in the System Prompt

A common fix is a line in the system prompt like "Only return data for the current user." It holds up until someone writes a message that convinces the model otherwise. Prompt injection is still an open problem, so the model can be talked out of any rule that lives only in the prompt.

Vector Search Without Permission Filters

You add RAG, documents get embedded and stored, and the agent searches them. The documents had owners in your main tables, but the vector index often doesn't know about them. A search for "contract renewal terms" can then pull a chunk from another customer's contract.

The same vector search with and without permission filters

AI Agent Access Control in the Data Layer

Each of these goes away once the rules live next to the data and every request from the agent carries the user's identity. After that, the prompt and the model's choice of tool stop being your line of defense. The database only hands back rows the user is allowed to see.

Row Level Security for AI Agents

Postgres has had row level security (RLS) since version 9.5. You turn it on for a table and write a policy:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

CREATE POLICY invoices_owner ON invoices
  FOR SELECT
  USING (user_id = current_setting('app.user_id', true)::uuid);

From now on, Postgres adds this condition to every query on invoices, whether it comes from your app or from an agent's tool call. The table owner and superusers skip RLS by default, so the connection your API uses shouldn't be one of them.

The policy reads app.user_id. The second argument in current_setting makes a missing value come back as NULL, so a request without a user gets no rows instead of an error. Your backend should set the value only after verifying the user's JWT, and it should be scoped to the transaction:

BEGIN;
SELECT set_config('app.user_id', $1, true);  -- true = this transaction only
SELECT id, amount, status FROM invoices;
COMMIT;

The true flag in set_config works like SET LOCAL. If you set the value for the whole session instead and run a pooler like PgBouncer in transaction mode, it can stay on the connection and show up in the next user's queries.

Passing the User's Token to the Agent

The agent has to send the user's token with every tool call. Your app usually has it already, since the user is signed in. Pass it into the agent run and have each tool attach it to its requests. The backend verifies the token, reads the user ID from it, and the policies take it from there.

Background agents are different. A job that runs at 3am might find the user's token already expired. Give jobs like that their own role with only the access they need, and keep that role away from user-facing agents.

How to Set Up AI Agent Permissions in Nhost

Nhost gives you Postgres, auth, storage, and an API in one project. Permissions follow the same idea as RLS, but they're enforced in the API layer. You set them per role and per table in the dashboard, and the JWT handling from the previous section is already done for you.

Step 1: Add a Permission Rule to the Table

Open the table in the dashboard, pick the user role, and set a row filter for select:

{
  "user_id": { "_eq": "X-Hasura-User-Id" }
}

X-Hasura-User-Id comes from the signed-in user's token, so this reads as "only rows where user_id is the current user." You can also pick which columns the role gets to see.

Nhost dashboard permissions editor: user role, select, row filter user_id equals X-Hasura-User-Id

Step 2: Call the API from the Agent With the User's Token

A tool in your agent is a regular function that sends the user's access token along:

async function getInvoices(userAccessToken: string) {
  const res = await fetch(NHOST_API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${userAccessToken}`,
    },
    body: JSON.stringify({ query: '{ invoices { id amount status } }' }),
  })
  return res.json()
}

The query asks for all invoices, and Anna's token turns that into Anna's invoices. The agent code has no permission checks in it at all.

Step 3: Keep Semantic Search Under the Same Rules

For the vector search problem, Nhost Graphite adds embeddings and semantic search on top of your Postgres data, and search results go through the same permission rules as your tables. When the agent searches for "my unpaid invoices," it searches only the current user's data.

Does This Stop Prompt Injection?

No. Someone can still talk the agent into a weird answer or an off-topic rant.

If an attacker convinces the agent to "list every invoice in the system," the query still runs as the user, and the user only gets their own rows back. The worst case is an agent that misbehaves with data the user could already see.

A prompt injection still runs as the user and only returns that user's rows

For actions with real-world effects, like refunds or deleting records, add a confirmation step on top. Permissions decide what the agent is allowed to touch. They don't decide whether a particular action makes sense.

AI Agent Access Security Best Practices

  • Give user-facing agents the user's token. Admin keys are for migrations and scripts.
  • Enforce access rules in the database or the API. Instructions in the prompt are a nice extra on top.
  • Filter vector search by owner before results reach the model.
  • Start agents read-only and add write tools one at a time.
  • Log every tool call together with the user it ran for, so you can answer "what did the agent do for Anna last Tuesday?"

Wrapping Up

An agent that acts for a user should look like that user to your backend. Once it does, you can stop writing permission rules into prompts and let the database handle access, the same way it already does for the rest of your app.

If you want to try this setup without wiring up JWTs and policies yourself, Nhost gives you Postgres, auth, and permissions in one project.

And if you handle agent permissions differently, I'd like to hear how in the comments.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.