Skip to main content

Snowflake Setup

Connecting Snowflake to Caddi with a read-only agent user

Caddi connects to Snowflake as a dedicated agent user that authenticates with a key pair, not a password. Your Snowflake admin sets this up once: a user, a role, a warehouse to run on, a read-only session policy, and read grants on the schemas Caddi should see. Snowflake itself then enforces that Caddi can only read, whatever else the role is granted.

This guide sets up a read-only connection. Leave Write Schema blank when you connect. Writing to Snowflake uses a separate setup, which your Caddi contact will provide when you're ready.

Why Caddi uses a read-only agent user

Caddi runs automated workflows, some of them written with AI. We want your data protected by Snowflake's own controls, not only by Caddi's software. This setup uses two Snowflake features built for exactly this.

An agent user. SERVICE_AGENT is Snowflake's user type for automated agents that act under their own identity (generally available since July 2026). It can't sign in with a password or SSO, and every session it opens is marked as an agent session. See Agent identity.

A read-only ceiling. A Restricted Session Scope caps what an agent session can do. Snowflake grants the session only the privileges that are both granted to its role and allowed by the scope. Snowflake's built-in SNOWFLAKE$DATA_READ scope allows reading tables and views and refuses everything else. Your admin attaches it to Caddi's user with a session policy.

Why this is the safest way to connect:

  • Snowflake enforces it, not Caddi. If anything Caddi sends tried to change data, Snowflake refuses it with an access control error.

  • It holds even when grants are broader than intended. Every Snowflake role inherits the PUBLIC role, and grants pile up over time. Role grants alone are only as tight as the loosest of them. The ceiling caps all of them.

  • It covers every way of running SQL. Snowflake can run several actions in one statement, through chained statements, scripting blocks and inline procedures. Each of those still runs inside the ceiling.

  • Caddi can't loosen it. The scope can't be changed from within a session, and the agent user can't modify its own settings. Only your admin can.

  • You can see and govern agent activity. Caddi's queries are labeled as agent queries in your query history. Any masking or row access policies you write for agents also apply to Caddi.

We tested this setup ourselves. With the scope in place, Snowflake refused inserts, updates, deletes, new tables and schemas, chained statements, scripting blocks, inline procedures, and changes to the user's own settings. It refused them even on a table the role owned. Reading data and looking up tables and columns kept working.

Before you start

Who needs to be involved:

  • A Snowflake admin who can use the ACCOUNTADMIN role to create the user, role and session policy, grant access, and create a warehouse if you choose to.

  • A Caddi organization owner or admin, to enter the values in Step 8.

You also need a terminal with OpenSSL to create the key pair: Terminal on macOS or Linux, or a WSL (Ubuntu) or Git Bash shell on Windows.

Decide which schemas Caddi should read before you start. Access is granted schema by schema. The session policy also needs a home: a database and schema that Caddi has no access to. Step 5 creates CADDI_GOVERNANCE.POLICIES, or you can use an existing governance schema.

What you will end up with:

Caddi field

Example

Where it comes from

Account Identifier

acme-prod

Step 1

Private Key

Contents of caddi_rsa_key.p8

Step 2

Warehouse

CADDI_WH, or a warehouse you already have

Step 3

Service User

CADDI_SVC

Step 4

Role

CADDI_ROLE

Step 4

Step 1: Find your account identifier

Caddi needs your account identifier in orgname-accountname form. Run this in a Snowsight worksheet:

SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME();

It returns something like ACME-PROD. Use that value as is; case doesn't matter. The account locator (such as xy12345.us-east-1) and the account URL won't work.

Step 2: Create the key pair

Two OpenSSL commands create a private key, which you give only to Caddi, and a public key, which you register on the Snowflake user. See Snowflake's key-pair authentication guide for background.

openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out caddi_rsa_key.p8 -nocrypt
openssl rsa -in caddi_rsa_key.p8 -pubout -out caddi_rsa_key.pub

The second command prints writing RSA key; that's expected.

File

What it is

Who gets it

caddi_rsa_key.p8

Private key, starts with -----BEGIN PRIVATE KEY-----

Caddi only (Step 8)

caddi_rsa_key.pub

Public key, starts with -----BEGIN PUBLIC KEY-----

Snowflake (Step 4)

The private key has no passphrase because Caddi has to read it; Caddi doesn't accept passphrase-protected keys. Protect the file like a password and never email it.

Print the public key as a single line, ready to paste into Step 4:

grep -v -- '-----' caddi_rsa_key.pub | tr -d '\n'

Step 3: Choose a warehouse

Caddi's queries run on a Snowflake warehouse. You can create one just for Caddi, or let Caddi use a warehouse you already have. Either way, Caddi's data access is the same; the choice is about cost and isolation.

Why create a dedicated warehouse (what we recommend for most firms):

  • Clear costs. Caddi's usage shows up as its own warehouse on your Snowflake bill.

  • No contention. Caddi's queries never queue behind your team's work, and your team's queries never queue behind Caddi's.

  • Its own settings. You can size it, suspend it, or put a resource monitor on it without touching a warehouse other people rely on.

  • Easy to switch off. Suspending or dropping it stops Caddi's compute without affecting anyone else.

Why use an existing warehouse:

  • It can cost less. Each warehouse bills separately, with a 60-second minimum every time it resumes. If you already have a warehouse running most of the day and Caddi's queries are occasional, they run on time you're already paying for.

  • Less to manage. No new object, and no exception needed if your firm restricts who can create warehouses.

If you share a warehouse, Caddi's queries run with its settings: a larger warehouse costs more per second, and a long auto-suspend keeps it running longer after each Caddi query wakes it. You can still find Caddi's queries in query history by filtering on the CADDI_SVC user.

Option A: create a dedicated warehouse. Extra-small is enough for Caddi. It suspends after 60 seconds idle and uses 1 credit per hour while running, billed per second.

USE ROLE ACCOUNTADMIN;

CREATE WAREHOUSE IF NOT EXISTS CADDI_WH
WAREHOUSE_SIZE = XSMALL
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE
INITIALLY_SUSPENDED = TRUE;

Option B: use an existing warehouse. List your warehouses and note the name of the one Caddi should use:

SHOW WAREHOUSES;

The size and auto_suspend columns show what Caddi's queries will run with.

Step 4: Create the role and agent user

Run this in a Snowsight worksheet, replacing the placeholder with the one-line public key from Step 2. If you chose an existing warehouse in Step 3, replace CADDI_WH with its name in both places. Every statement is safe to re-run.

USE ROLE ACCOUNTADMIN;

CREATE ROLE IF NOT EXISTS CADDI_ROLE;
GRANT USAGE ON WAREHOUSE CADDI_WH TO ROLE CADDI_ROLE;

CREATE USER IF NOT EXISTS CADDI_SVC
TYPE = SERVICE_AGENT
RSA_PUBLIC_KEY = '<public key from Step 2>'
DEFAULT_ROLE = CADDI_ROLE
DEFAULT_WAREHOUSE = CADDI_WH
DEFAULT_SECONDARY_ROLES = ()
STATEMENT_TIMEOUT_IN_SECONDS = 900;
GRANT ROLE CADDI_ROLE TO USER CADDI_SVC;

What each part does:

  • TYPE = SERVICE_AGENT: makes CADDI_SVC an agent user. It authenticates only with the key pair, and every session it opens is an agent session, which is what lets the read-only ceiling in Step 5 apply.

  • DEFAULT_SECONDARY_ROLES = (): Caddi acts only with CADDI_ROLE. The session policy in Step 5 enforces this as well.

  • STATEMENT_TIMEOUT_IN_SECONDS = 900: Snowflake stops any single Caddi query after 15 minutes, on whichever warehouse it runs. If the warehouse has a lower timeout of its own, the lower one applies.

  • GRANT USAGE ON WAREHOUSE: lets Caddi run queries on the warehouse and resume it when it's suspended. It doesn't let Caddi resize, suspend or change it.

If CADDI_SVC already exists, CREATE USER IF NOT EXISTS leaves it unchanged. To update an existing user, run ALTER USER CADDI_SVC SET TYPE = SERVICE_AGENT; or ALTER USER CADDI_SVC SET RSA_PUBLIC_KEY = '...';

Step 5: Make the connection read-only

Create the session policy and attach it to CADDI_SVC. If you use an existing governance schema, replace CADDI_GOVERNANCE.POLICIES throughout.

USE ROLE ACCOUNTADMIN;

CREATE DATABASE IF NOT EXISTS CADDI_GOVERNANCE;
CREATE SCHEMA IF NOT EXISTS CADDI_GOVERNANCE.POLICIES;

CREATE SESSION POLICY IF NOT EXISTS CADDI_GOVERNANCE.POLICIES.CADDI_READ_ONLY
ALLOWED_SECONDARY_ROLES = ()
AGENT_RESTRICTED_SESSION_SCOPE = 'SNOWFLAKE$DATA_READ';

ALTER USER CADDI_SVC SET SESSION POLICY CADDI_GOVERNANCE.POLICIES.CADDI_READ_ONLY;
  • AGENT_RESTRICTED_SESSION_SCOPE = 'SNOWFLAKE$DATA_READ': Snowflake's built-in read-only ceiling. Caddi can read tables and views and look up tables and columns. Everything else is refused, including inserts, updates, deletes, creating or dropping objects, grants, and running procedures or functions.

  • ALLOWED_SECONDARY_ROLES = (): Snowflake ignores any other role granted to CADDI_SVC.

  • Keep the policy out of Caddi's reach. Don't grant CADDI_ROLE any access to the database or schema that holds it.

  • The ceiling applies only while the policy is attached. If you unset it, Caddi's access falls back to whatever CADDI_ROLE is granted.

  • A session policy set on a user replaces any account-level session policy for that user.

Step 6: Grant read access

Run this once for each schema Caddi should read, replacing MY_DB and MY_SCHEMA:

GRANT USAGE ON DATABASE MY_DB TO ROLE CADDI_ROLE;
GRANT USAGE ON SCHEMA MY_DB.MY_SCHEMA TO ROLE CADDI_ROLE;
GRANT SELECT ON ALL TABLES IN SCHEMA MY_DB.MY_SCHEMA TO ROLE CADDI_ROLE;
GRANT SELECT ON ALL VIEWS IN SCHEMA MY_DB.MY_SCHEMA TO ROLE CADDI_ROLE;
GRANT SELECT ON FUTURE TABLES IN SCHEMA MY_DB.MY_SCHEMA TO ROLE CADDI_ROLE;
GRANT SELECT ON FUTURE VIEWS IN SCHEMA MY_DB.MY_SCHEMA TO ROLE CADDI_ROLE;
  • Future grants make tables and views added to the schema later readable too, without another request to your admin.

  • Grant per schema, not database-wide. If a schema has its own future grants, Snowflake ignores database-level future grants for that schema, and new tables silently stay hidden from Caddi.

  • Data shared into your account, for example by a custodian or data provider, is granted per database instead: GRANT IMPORTED PRIVILEGES ON DATABASE SHARED_DB TO ROLE CADDI_ROLE;

Step 7: Check the setup

Caddi checks that the private key is readable when you save the connection, but it doesn't sign in to Snowflake at that point. These checks catch a mismatched key, a missing policy or a missing grant now, rather than the first time a workflow runs.

It's an agent user with the right key. Run:

DESC USER CADDI_SVC;

TYPE must be SERVICE_AGENT. Then compute your public key's fingerprint:

openssl rsa -pubin -in caddi_rsa_key.pub -outform DER 2>/dev/null | openssl dgst -sha256 -binary | openssl enc -base64

The user's RSA_PUBLIC_KEY_FP must be SHA256: followed by exactly that output.

The read-only policy is attached. Run:

SELECT policy_name, policy_kind
FROM TABLE(CADDI_GOVERNANCE.INFORMATION_SCHEMA.POLICY_REFERENCES(
REF_ENTITY_NAME => 'CADDI_SVC', REF_ENTITY_DOMAIN => 'user'));

It must list CADDI_READ_ONLY as a SESSION_POLICY.

The grants are in place. Run:

SHOW GRANTS TO ROLE CADDI_ROLE;

You should see USAGE on your warehouse and on each database and schema, and SELECT on the tables and views.

Nothing unexpected comes from PUBLIC. Every Snowflake role inherits the PUBLIC role. With the read-only ceiling in place, PUBLIC can widen what Caddi can read, but never what it can change. Review it:

SHOW GRANTS TO ROLE PUBLIC;

Step 8: Connect Snowflake in Caddi

This step must be completed by a Caddi organization owner or admin.

  1. In Caddi, open Settings, then Integrations.

  2. Under Connectors you can configure, find Snowflake - Custom Key Pair and click Configure.

Caddi field

What to enter

Account Identifier

The value from Step 1, for example acme-prod

Service User

CADDI_SVC

Private Key

The full contents of caddi_rsa_key.p8, including the BEGIN and END lines

Role

CADDI_ROLE

Warehouse

CADDI_WH, or the existing warehouse you chose in Step 3

Write Schema (optional)

Leave blank

To copy the private key on macOS without opening it:

pbcopy < caddi_rsa_key.p8

On Windows, open the file in Notepad and copy everything.

Click Save Configuration. Then delete your local copy of caddi_rsa_key.p8 or move it into your firm's secrets vault. Keep caddi_rsa_key.pub: it isn't secret, and the fingerprint check in Step 7 uses it.

How the connection behaves

  • Snowflake keeps it read-only. While the session policy is attached, Snowflake refuses any change Caddi's queries attempt, whatever CADDI_ROLE or PUBLIC is granted.

  • Caddi reads only what CADDI_ROLE can read, plus anything granted to PUBLIC. To narrow access, revoke grants from CADDI_ROLE.

  • Caddi's queries are labeled as agent queries. In query history, the agent_type column shows EXTERNAL_AGENT. Masking and row access policies that check IS_AGENT_ACTIVATED() apply to Caddi.

  • Warehouses aren't limited by the ceiling. CADDI_SVC can use any warehouse CADDI_ROLE has USAGE on, including through PUBLIC. Caddi always runs on the warehouse entered in Step 8.

  • Caddi signs short-lived tokens with the key. Each token lasts under an hour and is renewed automatically. The private key is stored encrypted and never shown again.

  • Editing the connection. Re-enter the private key whenever you save changes. Caddi never displays stored secrets.

  • Changing the account identifier. Remove the connection and add it again; editing the identifier on an existing connection doesn't take effect.

  • Network policies. If your account limits sign-in by IP address, ask your Caddi contact for Caddi's outbound IP addresses and allow them for CADDI_SVC.

  • Revoking access. ALTER USER CADDI_SVC SET DISABLED = TRUE; stops Caddi immediately. To remove everything, unset the session policy from the user, then drop the user, the role, the session policy, and the warehouse if you created one for Caddi.

Rotating the key

Snowflake lets a user hold two public keys, so you can rotate without downtime:

  1. Create a new key pair with the Step 2 commands, using new file names.

  2. Register it as the second key: ALTER USER CADDI_SVC SET RSA_PUBLIC_KEY_2 = '...';

  3. Edit the Snowflake connection in Caddi, paste the new private key, and save.

  4. Wait an hour, so tokens signed with the old key have expired, then remove it: ALTER USER CADDI_SVC UNSET RSA_PUBLIC_KEY;

Next time, put the new key in RSA_PUBLIC_KEY and unset RSA_PUBLIC_KEY_2.

Troubleshooting

Symptom

Likely cause

Fix

Saving fails: private key "is not a PEM or base64 key"

Something other than the private key was pasted, such as the public key

Paste the full contents of caddi_rsa_key.p8

Saving fails: error "parsing Snowflake private key"

The key has a passphrase (it starts with BEGIN ENCRYPTED PRIVATE KEY)

Create a new pair with the Step 2 commands

Saving fails: "Snowflake private key is not RSA"

The key isn't an RSA key

Create a new pair with the Step 2 commands

"JWT token is invalid"

The public key on CADDI_SVC doesn't match the private key in Caddi, or the account identifier or user name is wrong

Run the fingerprint check in Step 7 and recheck the identifier from Step 1. If you replaced the key in Snowflake, paste the new private key into the Caddi connection too.

"Restricted session scope: POLICY does not include … access"

A query tried to change something. This is the read-only ceiling working.

Nothing to fix. If you want Caddi to write, ask your Caddi contact about the write setup.

Warehouse "does not exist or not authorized"

CADDI_ROLE has no USAGE on it, or the name in Caddi differs

Grant USAGE on the warehouse to CADDI_ROLE and check the name in Caddi

Table "does not exist or not authorized"

Its schema isn't granted

Run Step 6 for that schema

A new table isn't visible

Its schema has no future grants

Run Step 6 for that schema

IP "is not allowed to access Snowflake"

A network policy blocks Caddi

Allow Caddi's outbound IP addresses for CADDI_SVC

Still stuck? Contact Caddi support with the exact error message you're seeing and the time it occurred.

Did this answer your question?