Tutorial: Governance enforcement in a sandbox

This tutorial demonstrates a key security property of Cortex Sandboxes: governance policies are enforced at the Snowflake layer, not the application layer. Column masking policies and row access policies apply inside a sandbox exactly as they apply to any other query — code running in a container cannot bypass them.

When you finish, you will have seen the same query return masked results both outside and inside a sandbox, confirming that a sandbox provides isolation without bypassing the governance your organization has already put in place.

Time to complete: about 15 minutes.

Prerequisites

  • The snowflake-sandbox-python SDK installed and a Snowflake connection configured. See Cortex Sandboxes quickstart.
  • Privileges to create tables and masking policies in a schema you own.

Step 1: Set up a demo table with masking policies

Create a table with columns that hold sensitive data, then attach masking policies so the email and salary fields are hidden from non-privileged roles.

Replace <your_database> and <your_schema> with your own database and schema.

-- Create the demo table
CREATE TABLE <your_database>.<your_schema>.DEMO_EMPLOYEES (
    employee_id  NUMBER,
    full_name    VARCHAR,
    email        VARCHAR,
    department   VARCHAR,
    salary       NUMBER
);

INSERT INTO <your_database>.<your_schema>.DEMO_EMPLOYEES VALUES
(1, 'Alice Chen',   'alice.chen@example.com',   'Engineering', 142000),
(2, 'Bob Torres',   'bob.torres@example.com',   'Marketing',    98000),
(3, 'Carol White',  'carol.white@example.com',  'Engineering', 138000),
(4, 'David Park',   'david.park@example.com',   'Finance',     115000),
(5, 'Eva Smirnova', 'eva.smirnova@example.com', 'Engineering', 145000);

-- Column masking: only ACCOUNTADMIN and SYSADMIN see real email addresses
CREATE OR REPLACE MASKING POLICY email_mask AS (val VARCHAR) RETURNS VARCHAR ->
  CASE
    WHEN CURRENT_ROLE() IN ('ACCOUNTADMIN', 'SYSADMIN') THEN val
    ELSE REGEXP_REPLACE(val, '[^@]+', '****', 1, 1)
  END;

-- Column masking: only ACCOUNTADMIN and SYSADMIN see real salaries
CREATE OR REPLACE MASKING POLICY salary_mask AS (val NUMBER) RETURNS NUMBER ->
  CASE
    WHEN CURRENT_ROLE() IN ('ACCOUNTADMIN', 'SYSADMIN') THEN val
    ELSE 0
  END;

-- Apply the policies
ALTER TABLE <your_database>.<your_schema>.DEMO_EMPLOYEES
    MODIFY COLUMN email  SET MASKING POLICY email_mask;

ALTER TABLE <your_database>.<your_schema>.DEMO_EMPLOYEES
    MODIFY COLUMN salary SET MASKING POLICY salary_mask;

Step 2: Confirm masking outside a sandbox

Run the same SELECT as your regular role (one that is neither ACCOUNTADMIN nor SYSADMIN). You should see masked values in the email and salary columns.

SELECT employee_id, full_name, email, department, salary
FROM   <your_database>.<your_schema>.DEMO_EMPLOYEES
ORDER  BY employee_id;

Expected result:

EMPLOYEE_ID  FULL_NAME      EMAIL              DEPARTMENT    SALARY
-----------  -------------- ------------------ -----------   ------
1            Alice Chen     ****@example.com   Engineering   0
2            Bob Torres     ****@example.com   Marketing     0
3            Carol White    ****@example.com   Engineering   0
4            David Park     ****@example.com   Finance       0
5            Eva Smirnova   ****@example.com   Engineering   0

The salary column returns 0 and email shows only the domain portion — both policies applied to your role.

Step 3: Run the same query inside a sandbox

Now run the identical SELECT from inside a sandbox. Save the following as main.py and replace the database and schema placeholders.

# main.py
from snowflake.sandbox import Sandbox

DB     = "<your_database>"
SCHEMA = "<your_schema>"

# QUERY_SCRIPT runs inside the sandbox. DB and SCHEMA are passed as arguments
# so the script is a plain string with no nested f-string quoting tricks.
QUERY_SCRIPT = """
import sys

from snowflake.sandbox import get_snowflake_connection

DB, SCHEMA = sys.argv[1], sys.argv[2]

conn = get_snowflake_connection()
cur = conn.cursor()

# Report the identity the policies actually see, rather than trusting the
# injected environment: CURRENT_ROLE() is what row access and masking are
# evaluated against.
cur.execute("SELECT CURRENT_USER(), CURRENT_ROLE()")
who = cur.fetchone()
print("Running as:", who[0], "/", who[1])
print()

cur.execute(
    "SELECT employee_id, full_name, email, department, salary "
    f"FROM {DB}.{SCHEMA}.DEMO_EMPLOYEES ORDER BY employee_id"
)

print(f"{'employee_id':<13}{'full_name':<16}{'email':<27}{'department':<14}{'salary'}")
print("-" * 75)
for r in cur.fetchall():
    print(str(r[0]).ljust(13) + str(r[1]).ljust(16) + str(r[2]).ljust(27) + str(r[3]).ljust(14) + str(r[4]))

conn.close()
"""

print("Creating sandbox and running governance query...")
with Sandbox.create() as sb:
    result = sb.exec(["python", "-c", QUERY_SCRIPT, DB, SCHEMA], timeout=60.0)
    print(result.stdout)

How to run this example

python main.py

Expected output:

Creating sandbox and running governance query...
Running as: MY_USER / MY_ROLE

employee_id   full_name       email                      department    salary
---------------------------------------------------------------------------
1             Alice Chen      ****@example.com           Engineering   0
2             Bob Torres      ****@example.com           Marketing     0
3             Carol White     ****@example.com           Engineering   0
4             David Park      ****@example.com           Finance       0
5             Eva Smirnova    ****@example.com           Engineering   0

The output inside the sandbox is identical to the output from Step 2. The masking policies applied in both cases.

Create the sandbox with the same non-privileged role you used in Step 2. The sandbox runs with the role of the session that created it, so creating it as ACCOUNTADMIN or SYSADMIN makes these policies return the real values — the query would come back unmasked, which is the policy working as written rather than governance being bypassed.

What this demonstrates

The sandbox inherits your session identity. When the code inside runs a query, Snowflake evaluates the applicable masking and row access policies for your role — the same way it would for any other SQL session. There is no way for code inside a sandbox to see more data than your role already permits.

This property is what makes Cortex Sandboxes safe for running AI-generated code over sensitive tables:

  • An LLM-generated query that tries SELECT * FROM sensitive_table returns only what your role can see.
  • An agent that iterates over query results cannot exfiltrate unmasked data, because the unmasked values are never returned to the container.
  • Audit logs record all queries the sandbox runs, attributed to your user and role, just as they would for any other session.

See Cortex Sandboxes concepts and security for a full explanation of the security model.

Going further