Fence Snowflake Sensitive Schemas by Data Domain
Fences customer-designated sensitive data domains inside a Snowflake warehouse by inspecting the SQL text the agent is about to run — not by tool name, which…
- Direction
- ingress
- Rego package
snowflake.ingress.fence_sensitive_schemas- App
- snowflake
- Bundles
- soc2hipaapci-dssgdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:1679f0033186a3e591fb29d8086f5d1203cccffb675d791e70e7d7a11545709a
snowflakefence-sensitive-scopesingresssoc2hipaapci-dssgdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny sensitive-domain references for callers outside the mapped group, allow otherwise
Package: snowflake.ingress.fence_sensitive_schemas
What it does
Fences customer-designated sensitive data domains inside a Snowflake warehouse by
inspecting the SQL text the agent is about to run — not by tool name, which on
Snowflake carries no stable semantics (see Tool name matching). The policy carries one
placeholder map, sensitive_domains, pairing a schema/table name prefix with the
IdP group required to touch that domain:
PII_→pii-clearedPHI_→phi-clearedHR_→hrFINANCE_→finance
It reads two argument shapes and applies two independent controls at ingress:
- Domain fence (group-gated). It scans the SQL
query(andstatement) argument and the natural-languagemessageargument (CORTEX_ANALYST_MESSAGE, best-effort) for any identifier beginning with a fenced prefix (e.g.PHI_LABRESULTS,ANALYTICS.FINANCE_LEDGER). If a fenced prefix is referenced and the caller'sinput.subject.claims.groupsdoes not include the mapped group, the call is denied. SELECT *fence (outright). If the SQL text performs a star-select (SELECT *,SELECT DISTINCT *,SELECT ALL *,SELECT TOP <n> *, the no-spaceSELECT*, or a table-qualifiedSELECT c.*) and references any fenced prefix, the call is denied for everyone — including cleared callers — forcing an explicit column list. This makes the agent state its intent and stops it from sweeping every column of a regulated table in a single call.
Group membership is read through object.get chains and fails closed: a missing, empty, or
malformed subject/claims/groups never grants a fenced domain (no group → not exempt).
Calls carrying no query/statement/message argument (e.g. list_databases,
describe_table) are not inspected and pass through untouched.
Compliance alignment
- HIPAA §164.502(b)/§164.514(d) — supports the minimum-necessary and role-based-limits
standard: agent access to a PHI-bearing domain is gated to its mapped group on the MCP path,
and the
SELECT *fence forces column-level intent so a call cannot pull more PHI than named; §164.308(a)(4) — supports information access management by authorizing sensitive-domain access via IdP group; §164.522(a) — the domain map can encode agreed-to restrictions on specific regulated schemas. - PCI DSS 7.2.6 — supports restricting programmatic query access to stored cardholder data
by role: fence the
FINANCE_/CHD prefix so only the mapped group can query it through an agent. - SOC 2 C1.1 — supports identification and protection of confidential information by gating agent SQL against designated confidential domains; P4.1 — supports limiting personal- information use to identified purposes by keeping PI-bearing schemas behind role fences.
- GDPR Art. 9 — supports special-category protection by fencing schemas holding health, HR, or other Art. 9 data; CPRA §1798.121 — supports the right to limit use of sensitive personal information by fencing SPI schemas to a minimal group; GDPR Art. 5(1)(b) — supports purpose limitation on the agent channel.
Why ingress and least-privilege
This is the minimum-necessary / least-privilege control for the warehouse's data plane: it stops a regulated-schema read before it executes, rather than masking a response the query already produced. It pairs with an egress redaction backstop (see Composition) for defense in depth — because which specific tables hold regulated data is not knowable from the wire, egress redaction catches leaks from schemas an operator has not yet pinned here.
Tool name matching
This policy does not match tool names. Snowflake MCP servers expose the dangerous surface as a
SQL string inside a single argument, and there are no stable canonical tool names: the managed
server's tools are admin-named (semantics live in a type that is not visible on the wire), the
Labs server derives Cortex tool names from config, and each community/Labs SQL tool takes the SQL
as an argument. Matching tool names would therefore be neither portable nor sound. Instead the
policy inspects the arguments every SQL/analyst tool uses:
- SQL execution/read tools (
run_snowflake_query,read_query,write_query,create_table, and the managed server's SQL-execution tool) take the SQL text underquery(astatementkey is also checked defensively). - Cortex Analyst tools (
CORTEX_ANALYST_MESSAGE-typed) take a natural-languagemessage.
Any call carrying none of these arguments is not inspected. Verify your server's argument names with the dump-input debug technique before relying on this in production.
Argument shape
query/statement— the SQL text (string). Scanned for fenced prefixes and forSELECT *.message— the Cortex Analyst natural-language string. Scanned for fenced prefixes only (best-effort; a star-select has no meaning in NL).
Examples
Allowed — cleared caller, explicit columns
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "snowflake-run_snowflake_query", "type": "tool" },
"subject": { "sub": "google-apps|nurse@example.com", "claims": { "groups": ["phi-cleared"] } },
"payload": {
"name": "snowflake-run_snowflake_query",
"args": { "query": "SELECT patient_id, visit_date FROM PHI_RECORDS WHERE patient_id = 42" }
}
}
}
allow = true, no reason.
Denied — uncleared caller references a fenced domain
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "snowflake-run_snowflake_query", "type": "tool" },
"subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "snowflake-run_snowflake_query",
"args": { "query": "SELECT customer_id, email FROM PII_CUSTOMERS WHERE id = 5" }
}
}
}
allow = false, reason = "Access to the 'PII_' sensitive data domain requires the 'pii-cleared' IdP group. (...)".
Denied — SELECT * on a fenced schema, even for a cleared caller
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "snowflake-run_snowflake_query", "type": "tool" },
"subject": { "sub": "google-apps|analyst@example.com", "claims": { "groups": ["finance"] } },
"payload": {
"name": "snowflake-run_snowflake_query",
"args": { "query": "SELECT * FROM FINANCE_LEDGER" }
}
}
}
allow = false, reason = "SELECT * against a fenced sensitive schema is not allowed (...)".
Composition
This policy is single-purpose. Curated companions for the Snowflake data plane:
guard-warehouse-sql(ingress) — deny DDL/DML/GRANTand bulk-export constructs (COPY INTO @,CREATE STAGE) so a fenced-out caller cannot pivot to exfiltration.redact-pii-egress(egress) — redact SSN/PAN/email patterns from returned rows: the defense-in-depth backstop for regulated tables that have not yet been pinned intosensitive_domains, and for Cortex results this policy inspected only best-effort.default-deny-unknown-tools(ingress) — allowlist the audited Snowflake tool names so an admin-renamed or newly-added tool cannot introduce an uninspected SQL path.
Known limitations
- Placeholder configuration. The prefixes (
PII_,PHI_,HR_,FINANCE_) and group names (pii-cleared,phi-cleared,hr,finance) are placeholders — replace them with your real schema/table naming convention and your IdP's group-claim values at import time. Which tables hold regulated data is not knowable from the wire (per the Snowflake landscape note), so pinning the naming convention is mandatory for this policy to do anything. - Convention-dependent, not ancestry-aware. Detection keys on a prefix at an identifier
boundary (
\bPREFIX), so it fences a regulated domain only when its tables/schemas actually carry the pinned prefix. A regulated table that does not follow the naming convention (CUSTOMERS_PII,patient_data) is not fenced. Enforce the naming convention in Snowflake, and rely on the egress redaction backstop for the residual. SELECT *detection is regex-based. It catchesSELECT *,SELECT DISTINCT *,SELECT ALL *,SELECT TOP <n> *(and combinations of those leading set-quantifier / row-limit tokens), table-qualifiedSELECT alias.*, and the no-spaceSELECT*form. Exotic forms still evade it: a*produced by a view, a comment or hint betweenSELECTand*(SELECT /*x*/ * FROM ...), or every column enumerated by name (arithmeticcol * 2is not flagged and is not a leak). The domain group-fence still applies to uncleared callers regardless; only a cleared caller could evade the star-select fence, and the egress backstop remains. A*reference inside a string literal may cause a conservative (fail-safe) denial.- Prefix detection can be evaded by identifier obfuscation. Detection matches the literal
fenced prefix at an identifier boundary in the wire text. A caller who constructs the
identifier dynamically — e.g. Snowflake
IDENTIFIER('PII' || '_CUSTOMERS')with the prefix split across concatenated string literals, or a variable/session bind — references the fenced table without the contiguous prefix ever appearing, so the domain fence does not fire and the call is allowed. This is a fundamental limit of wire-level SQL inspection. Pair withguard-warehouse-sql(to deny/scope dynamic-SQL constructs) and keep the egress redaction backstop in place; treat the ingress fence as one layer, not the sole control. messageinspection is best-effort. Cortex Analyst turns natural language into SQL server-side; the gateway sees only the NLmessage. This policy fences an NL request that literally names a fenced prefix, but cannot see the SQL the semantic model ultimately generates. Deny or tightly scope Cortex Analyst/Agent tools (seedefault-deny-unknown-tools) if that residual is unacceptable, and keep the egress redaction backstop in place.- Composite/opaque tools not reached.
CORTEX_AGENT_RUN-typed tools execute multi-step plans server-side; the gateway sees one opaque call and per-statement inspection cannot reach inside it. Deny agent tools and force the client to use granular, inspectable tools. groupsclaim must be an array of strings. A string-valued or otherwise malformed claim fails closed (fenced domains deny). If your IdP emits groups under a different claim name (e.g. a namespaced custom claim), updatecaller_groupsin the Rego.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package snowflake.ingress.fence_sensitive_schemas
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# ---------------------------------------------------------------------------
# Fence configuration — PLACEHOLDERS, replace at import time.
#
# Snowflake exposes no stable tool names and which tables hold regulated data
# is not knowable from the wire, so this policy fences by a customer-pinned
# schema/table NAME PREFIX -> the IdP group required to query that domain.
# Prefixes are matched case-insensitively at an identifier boundary (\bPREFIX);
# groups are compared case-insensitively against `subject.claims.groups`.
sensitive_domains := {
"PII_": "pii-cleared", # e.g. PII_CUSTOMERS, ANALYTICS.PII_PROFILES
"PHI_": "phi-cleared", # e.g. PHI_RECORDS, PHI_LABRESULTS
"HR_": "hr", # e.g. HR_EMPLOYEES, HR_COMP
"FINANCE_": "finance", # e.g. FINANCE_LEDGER, FINANCE_PAYROLL
}
# ---------------------------------------------------------------------------
# Identity — read groups via object.get chains so a missing subject/claims/
# groups fails closed (no group -> no access to a fenced domain).
caller_groups := object.get(object.get(object.get(input, "subject", {}), "claims", {}), "groups", [])
# True when the caller's groups claim (an array of strings) contains `group`.
# A malformed (non-array) claim makes the iteration fail -> fail closed.
caller_has_group(group) if {
some g in caller_groups
lower(g) == lower(group)
}
# ---------------------------------------------------------------------------
# Argument extraction — object.get everywhere. SQL text arrives under `query`
# (and, defensively, `statement`); Cortex Analyst text under `message`.
args := object.get(object.get(input, "payload", {}), "args", {})
sql_candidates contains t if {
t := object.get(args, "query", "")
t != ""
}
sql_candidates contains t if {
t := object.get(args, "statement", "")
t != ""
}
nl_candidates contains t if {
t := object.get(args, "message", "")
t != ""
}
# Prefix references are checked across both SQL and NL text; SELECT * only
# meaningfully applies to SQL text.
all_text_candidates := sql_candidates | nl_candidates
# This policy only inspects calls that carry a SQL query/statement or an
# analyst message; everything else passes through.
is_inspected_call if {
count(all_text_candidates) > 0
}
# ---------------------------------------------------------------------------
# Detection helpers.
# True when `text` references an identifier beginning with `prefix`. `\b`
# anchors the match to an identifier boundary (start of string, whitespace,
# `.`, `(`, quote, comma), so FINANCE_LEDGER and DB.PII_X match but a prefix
# buried mid-identifier (MY_PII_COL) does not.
domain_in_text(prefix, text) if {
regex.match(sprintf(`(?i)\b%s`, [prefix]), text)
}
# A fenced domain the caller is NOT cleared for is referenced in the request.
denied_domains contains prefix if {
some prefix, group in sensitive_domains
some t in all_text_candidates
domain_in_text(prefix, t)
not caller_has_group(group)
}
# A star-select against a fenced schema, in a single SQL statement. Catches
# `SELECT *`, `SELECT DISTINCT *`, `SELECT ALL *`, `SELECT TOP <n> *` (and any
# combination of those leading set-quantifier / row-limit tokens, in any order),
# table-qualified `SELECT alias.*`, and the no-space form `SELECT*` (valid SQL)
# so a missing space or a leading modifier cannot evade the fence. Each modifier
# alternative requires a following `\s+`, so identifiers like `all_flags`,
# `distinct_id`, or `top_customer` are NOT mistaken for a set quantifier.
select_star_on_sensitive if {
some t in sql_candidates
regex.match(`(?i)select\s*(?:(?:all|distinct|top\s+\d+)\s+)*(?:\w+\.)?\*`, t)
some prefix in object.keys(sensitive_domains)
domain_in_text(prefix, t)
}
# ---------------------------------------------------------------------------
# Allow rules.
# Any call this policy does not inspect passes through untouched.
allow if {
not is_inspected_call
}
# Inspected call with no fenced-domain violation and no star-select on a
# fenced schema.
allow if {
is_inspected_call
count(denied_domains) == 0
not select_star_on_sensitive
}
# ---------------------------------------------------------------------------
# Deny reasons.
reasons contains msg if {
some prefix in denied_domains
group := sensitive_domains[prefix]
msg := sprintf("Access to the '%s' sensitive data domain requires the '%s' IdP group. Ask your data platform admin for that entitlement, or contact InfoSec if this fence looks wrong.", [prefix, group])
}
reasons contains "SELECT * against a fenced sensitive schema is not allowed. List the specific columns you need so the access is minimum-necessary, then re-run. Contact your data platform admin if a fenced prefix is mislabeled." if {
select_star_on_sensitive
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Used in these guides
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa