How Do I Give Each AI Agent Its Own Identity and Least-Privilege Access to a Data Warehouse Over MCP?
Five layers that replace one shared API key with a named identity, a task-scoped role and a trail you can read
To give an AI agent least-privilege access to a warehouse over MCP, give every agent its own named warehouse identity, bind that identity to a role scoped to one task, and let the MCP server mint short-lived credentials for it instead of passing a shared key or the user's token through. The MCP authorization specification puts the MCP server in the OAuth resource-server role, which is where that scoping decision belongs. Skyflow's write-up on MCP data controls describes the failure we are fixing: agents end up over-permissioned through app-level access and static API keys.
Key Takeaways
- •One shared service key turns every agent into the same user, so you cannot scope, revoke or audit any single agent.
- •Least privilege for agents means one identity per agent and one role per task, not one broad role per team.
- •The MCP server should issue its own short-lived credentials for the warehouse. It should never forward the token it received from the client.
- •Masking and row policies keyed to the active role keep sensitive columns closed even when an agent's SQL is wrong.
- •Warehouse access history only helps if each query carries the agent and task that issued it.
Why MCP agents end up over-permissioned
The usual path is boring. A team wires an MCP server to the warehouse, creates one service account so the demo works, and hands it a role broad enough that no query fails. That account becomes the identity of every agent, every prompt and every user who reaches the server. The warehouse sees one principal doing everything, so its access controls have nothing to distinguish.
The second cause is where the check happens. When authorization lives in the application layer, the warehouse trusts whatever the server says, and a static API key carries no notion of who is asking or why. The Skyflow piece linked above names app-level access and static keys as the root of agent over-permissioning, and our experience with early deployments matches that description.
The third cause is that agents behave differently from analysts. A person runs a handful of queries they understand. An agent explores, retries and chains tools, so a broad role gets exercised far more widely than anyone planned. A role that was acceptable for one analyst is a larger exposure when a model can call it in a loop. Scope has to be set by the task, because the agent will use everything the role allows.
Layer 1: one warehouse identity per agent
Start by creating a distinct non-human principal per agent. In Snowflake that is a user created with TYPE = SERVICE, documented in the CREATE USER reference, which suits programmatic callers and does not allow password login. The identity should be named for the agent and its function, such as AGENT_INCIDENTS or AGENT_QUALITY, so a row in a log names a specific actor.
Authenticate that identity with a key pair or workload federation, not a password stored in an environment variable. Each agent gets its own key, so revoking one agent is a single ALTER USER that touches nothing else. This is the step that makes every later control meaningful, because roles, policies and audit rows all attach to the principal.
A minimal version looks like this:
- •
CREATE USER agent_incidents TYPE = SERVICE RSA_PUBLIC_KEY = '<public key>' DEFAULT_ROLE = agent_incidents_diagnose; - •
CREATE ROLE agent_incidents_diagnose; - •
GRANT ROLE agent_incidents_diagnose TO USER agent_incidents;
Keep the grant list short. If an agent legitimately needs two kinds of work, give it two roles and have the MCP server select one per task, rather than stacking everything onto one default.
Layer 2: a role scoped to the task, not the team
Roles should describe a job: diagnose a failed load, read freshness metadata, profile one schema. Each carries only the usage and select grants for that job. Snowflake's access control overview describes the model of privileges granted to roles and roles granted to users, and agent roles are ordinary roles in that model. The discipline is in how narrow you make them.
For a diagnosis task the grants might be USAGE on one database and schema, SELECT on the staging and metadata tables involved, and nothing on tables holding customer attributes. Write permissions belong in a separate role that the agent cannot select without an approval step. Read-only by default is a good property to inherit: our open-source edition, dw-claw, ships read-only, and writes arrive only with the Pro edition's approval workflows.
Avoid inheriting a parent role that already holds broad access, since inheritance quietly widens the task role. Review each agent role the way you review an IAM policy: list what it can touch, then remove anything the task does not require. A role that cannot see a table cannot leak it, whatever the prompt says.
Layer 3: short-lived credentials and no token passthrough
The identity and role are static. The credential that proves them should not be. Have the MCP server hold the agent's key in a secrets manager, sign a short-lived assertion per task, and let it expire when the task ends. A leaked credential then covers a narrow window and a narrow role, which is a much smaller incident than a leaked master key.
The MCP project's security best practices page covers the confused deputy problem and token passthrough, where a server accepts a token that was not issued to it and forwards it downstream. The guidance is that servers must validate that tokens were issued for them as the audience. Apply that here: the token a client presents to your MCP server authorizes the server, and the server then uses its own credential for the warehouse.
This separation is what lets you enforce per-agent scope at all. If the client's token flowed straight to the warehouse, the warehouse would see the user's authority rather than the agent's task. With the server in the middle, it can map a request to the right agent identity and role, and refuse anything outside that mapping before the warehouse is involved.
Layer 4: policies that follow the role
Roles decide which tables are visible. Column and row policies decide what a visible table reveals, and they should key off the active role so a mistaken query still returns safe values. In Snowflake that means a masking policy using IS_ROLE_IN_SESSION, attached to PII columns once, rather than per-query filtering that depends on the agent behaving.
- •
CREATE MASKING POLICY mask_email AS (val STRING) RETURNS STRING -> CASE WHEN IS_ROLE_IN_SESSION('PII_READER') THEN val ELSE '***MASKED***' END; - •
ALTER TABLE analytics.customers MODIFY COLUMN email SET MASKING POLICY mask_email; - •Grant
PII_READERto a human-reviewed role only, never to an agent task role.
With that in place, an agent that wanders into the customers table gets masked values, and the same query run by an authorized person returns clear text. The agent does not need to know the rule exists. That matters because prompts are a weak control: a policy enforced by the warehouse holds even when the model, the prompt or the tool description is wrong.
We treat masking as a backstop, not the primary control. The primary control is that the task role never receives the sensitive table in the first place, and the policy catches the cases where a grant was broader than intended.
Layer 5: attribution you can read later
Least privilege is only credible if you can show what each agent did. Snowflake's ACCESS_HISTORY view records which tables and columns each query read, and the account usage views keep that record for 365 days. A distinct user per agent means those rows are already attributed to the right principal.
Add the task. Set a query tag at session start, for example ALTER SESSION SET QUERY_TAG = '{"agent":"incidents","task":"INC-1042"}', so a row ties back to the ticket or prompt that caused it. Then questions like "which agent read the orders table last Tuesday, and for what" become a query instead of an investigation across server logs.
Review the trail on a schedule, not only after an incident. Look for roles whose grants are never exercised, which are candidates to shrink, and for denied statements, which show an agent reaching past its scope. Both feed back into layer 2, and over a few weeks the roles converge on what each task actually uses.
A rollout order that does not stall the team
Do not try to retrofit all five layers across every agent at once. Start with the agent that touches the most sensitive schema, give it its own service identity and a read-only task role, and leave the rest on the shared account while you watch its access history. You will find grants you did not know the shared role needed, and you will find ones it never used.
Next, move credential issuance into the MCP server and remove any path that forwards a client token to the warehouse. Then attach masking policies to the columns your governance owners already classify as sensitive. Add query tags last, since they are cheap and only pay off once identities are separated.
Retire the shared key only when the final agent has moved, and rotate it immediately after. A shared key that lingers as a fallback is the same exposure under a new name. Keep a short written list of which agent maps to which role, so a reviewer can check the mapping in minutes.
How this works in Data Workers
Data Workers runs each of its fifteen MCP-native agents as its own principal, so the Incidents Agent and the Quality Agent never share a credential. The Governance Agent generates RBAC policy drafts and PII classifications, and its output is what you would turn into the task roles and masking policies shown above. The open-source edition, dw-claw, is Apache 2.0, runs locally and is read-only by default.
The Pro edition adds writes, enterprise connectors, approval workflows and audit trails, so a write role is only active after a person approves the change. Enterprise adds SSO/SAML, multi-tenant isolation and VPC or on-prem deployment. Confirm the exact controls for your edition against the current product documentation before you rely on them in a compliance review.
The Snowflake examples in this post work with or without us. The roles, masking policies and query tags are standard warehouse features, and you can apply them to any MCP server today. What we add is the coordination across agents, so scoping and audit are consistent instead of reinvented per tool.
Frequently Asked Questions
Is a read-only role enough to make an agent safe?
No. A read-only role stops writes, but it can still read every table it is granted, including sensitive ones. Scope the role to the schemas the task needs, mask sensitive columns by role, and keep the audit trail on. Read-only is the starting point of least privilege, not the whole of it.
Should agents use the end user's identity or their own?
Use the agent's own identity for the warehouse, and record the human who triggered the task in the query tag. Passing the user's token through is the pattern the MCP security guidance warns against, because the server then forwards a token that was not issued to it. Separate identities also let you revoke one agent without touching a person.
How narrow should a task role be?
Narrow enough that you can list its grants in one screen and explain each. If a role needs a dozen schemas, the task is probably several tasks. Split it, then use the access history to confirm which grants were exercised, and drop the rest.
Do I still need masking if the role is already narrow?
Yes, as a backstop. Grants drift, and a role can end up with access to a table nobody meant to include. A masking policy keyed to the active role keeps the sensitive column closed in that case, and it costs one statement per column.