Most Snowflake security incidents are not exotic. They are an over-granted role, a service account with a password, a sensitive column nobody knew existed, or an inability to answer who accessed what.
Snowflake provides the controls to prevent all four. The platform work is rarely the hard part. The hard part is deciding what is sensitive, who should see it, and being able to demonstrate that the decision is enforced.
This final article covers the governance layer: authentication, data protection policies, classification, auditability, and recovery. It completes the architecture established in Part 1, where access was built on functional roles rather than individual grants.
Governance layers and the control that enforces each
|
Layer |
Question it answers |
Control |
|
Authentication |
Who may connect at all? |
Network and authentication policies; MFA; key pair for service users |
|
Access model |
Which objects can they reach? |
Access roles into functional roles; database roles; managed schemas |
|
Data protection |
What may they see inside? |
Masking policies; row access policies; tag-based masking |
|
Discovery |
Where is the sensitive data? |
Sensitive data classification; semantic and privacy tags |
|
Audit |
Can we prove enforcement? |
ACCESS_HISTORY; column lineage; policy references |
|
Continuity |
Do we still have the data? |
Time Travel; replication and failover groups |
Authentication Is Changing - Act Now
The most time-sensitive item in this article is not a best practice. It is a deadline.
Snowflake is deprecating single-factor password sign-ins. Accounts created after the 2024_08 behavior change bundle already require every human user authenticating with a password to enroll in MFA. Older accounts must configure this themselves. The opt-out an authentication policy with MFA_ENROLLMENT = OPTIONAL - is explicitly temporary.
If your organization has not planned this, it is now a migration project rather than a configuration change.
The critical distinction is between user types. MFA is for human users. Service users must authenticate another way - key pair, OAuth, or programmatic access tokens. A service account carrying a password that a person also knows is the single most common finding in a Snowflake security review.
Authentication policies can target user types directly:
|
CREATE AUTHENTICATION POLICY require_mfa_policy |
Setting ENFORCE_MFA_ON_EXTERNAL_AUTHENTICATION = 'ALL' also requires MFA for SSO users. By default Snowflake relies on your identity provider for that, which may or may not match your control expectations.
Three operational details matter as much as the policy itself.
Order of evaluation. Network policies are evaluated first, then authentication policies, then password policies, then session policies. A blocked IP stops evaluation before authentication is considered.
Prevent your own lockout. A user-level policy overrides the account-level one. Create a less restrictive policy for a small number of administrators before tightening the account.
Plan break-glass access. Generate one-time passcodes for a dedicated recovery user:
|
ALTER USER breakglass_user ADD MFA METHOD OTP COUNT = 5; |
Store those with the credential in a vault. Note that OTPs are not replicated to target accounts, deliberately, so each account needs its own. Passkeys and TOTP do replicate; Duo does not.
Protecting Columns and Rows
Role-based access control decides who can reach a table. Policies decide what they see inside it.
Masking policies operate on columns at query time. There is no static masking — the stored data is unchanged, and the policy rewrites the query.
|
CREATE MASKING POLICY email_mask AS (val STRING) RETURNS STRING -> |
Write one policy, apply it to thousands of columns of matching type. Crucially, this supports separation of duties: object owners can neither unset the policy nor view the protected data. That is the property secure views cannot provide.
One behavior catches almost everyone. The policy applies wherever the column is referenced projections, WHERE predicates, JOIN conditions, GROUP BY, ORDER BY. That is deliberate, to prevent de-anonymization through creative querying. But it produces a well-known anti-pattern:
|
-- The column is masked; the literal is not. This returns nothing useful. |
Snowflake masks one side of the comparison only. Filtering and joining on masked columns does not do what users expect, and this surfaces as "the data is wrong" rather than "the policy is working."
Row access policies filter which rows are returned. The important design property: Snowflake evaluates the policy using the policy owner's role, not the querying user's. Users can be filtered by a mapping table they have no privileges to read.
|
CREATE ROW ACCESS POLICY sales_region_policy AS (region VARCHAR) RETURNS BOOLEAN -> |
Row access policies are evaluated before masking policies, and a column cannot appear in both policy signatures.
Two practical limits. Performance degrades with mapping-table lookups and many policy arguments - simple CASE expressions perform best, and clustering on the filtered attribute helps on large tables. And row access policies disable the metadata shortcuts that make SELECT COUNT(*) instant, because Snowflake must now determine which rows you are permitted to count.
Before rolling a policy out, simulate it:
|
EXECUTE USING POLICY_CONTEXT(SNOWFLAKE$SESSION_ACTIVATED_ROLES => ('ANALYST')) |
Find the Sensitive Data First
Policies only protect columns you know about. Sensitive data classification finds the rest.
Snowflake assigns two categories to each identified column: a semantic category (what the attribute is - name, national identifier) and a privacy category (IDENTIFIER, QUASI_IDENTIFIER, or SENSITIVE). Results are written as system tags, SNOWFLAKE.CORE.SEMANTIC_CATEGORY and SNOWFLAKE.CORE.PRIVACY_CATEGORY.
The combination worth building is classification plus tag-based masking. Map the system tag to your own tag, attach a masking policy to that tag, and newly classified columns are protected automatically as data lands - without anyone remembering to apply a policy.
|
CREATE TAG governance.tags.pii ALLOWED_VALUES 'restricted', 'internal'; |
Note that a masking policy assigned directly to a column takes precedence over a tag-based one.
Classification runs on serverless compute and is billable. Views cost more to classify than tables and are excluded by default. An optional AI mode improves accuracy using an LLM, adding token costs.
Prove It
Governance you cannot evidence is governance you cannot defend in an audit.
ACCESS_HISTORY records, per statement, the objects read directly, the underlying base objects, the objects modified, DDL changes, and - valuable for audits - the policies that were enforced.
|
SELECT user_name, query_start_time, direct_objects_accessed |
The policies_referenced column answers "was this column masked when that user queried it?" without joining several other views. The objects_modified column carries column-level lineage, letting you trace a sensitive column into every table derived from it - which is how you discover that a protected field was copied into an unprotected one via CTAS.
That risk is worth stating plainly: a role authorized to see unmasked data can insert that data into a column with no policy. Classification and lineage are how you catch it.
Account-level ACCESS_HISTORY carries no extra cost. The organization-level view in ORGANIZATION_USAGE is a premium view with compute and storage charges.
Sharing Without Leaking
Secure Data Sharing exposes data without copying it, and policies follow the shared object. But policy conditions behave differently across an account boundary.
`CURRENT_ROLE()` and `CURRENT_USER()` return NULL in the consumer's account. The provider does not control roles there. A policy written with CURRENT_ROLE() silently stops discriminating once shared.
Use CURRENT_ACCOUNT(), or share a database role and have the policy call IS_DATABASE_ROLE_IN_SESSION(). Consumers then grant that database role to their own account roles.
Also note that consumers cannot apply policies to shared objects, and external functions cannot be invoked within a share - which rules out External Tokenization for shared data.
Recovery Is a Governance Control
Access control answers who can read data. Recovery answers whether you still have it.
Set Time Travel retention by data tier rather than accepting the default everywhere. Critical production tables warrant longer retention; transient staging tables may need none. Retention affects storage cost, and as covered in Part 3, it also governs how long a stream can go unconsumed before going stale.
Replication and failover groups extend protection across accounts and regions. Two constraints matter: failover requires Business Critical Edition or higher, and replicating databases containing policies fails if a target account is on a lower edition or if a policy reference dangles across databases. Replication groups avoid the dangling-reference problem.
Finally, assign ownership. Someone must own policy changes, access reviews, classification exceptions, and recovery testing. Controls that nobody owns decay quietly.
Enterprise Readiness Checklist
Authentication
- MFA enforced for human users, with a migration plan off single-factor passwords.
- Service users authenticate by key pair, OAuth, or programmatic access tokens - never a shared password.
- Separate authentication policies for PERSON and SERVICE user types.
- Network policies defined and understood as the first evaluation layer.
- Break-glass user established with OTPs stored in a vault, and an unlock path tested.
- A less restrictive admin policy exists to prevent account-wide lockout.
Access and data protection
- Privileges granted to access roles and consumed through functional roles.
- Database roles used where access should stay scoped to one database.
- Managed access schemas used where grant decisions must stay centralized.
- Masking policies applied to sensitive columns, with filter and join behavior explained to users.
- Row access policies applied where row-level entitlement is required.
- Policies simulated with POLICY_CONTEXT before production rollout.
Discovery and audit
- Sensitive data classification enabled on databases likely to hold personal data.
- Tag-based masking wired to classification so new columns inherit protection.
- ACCESS_HISTORY monitored for sensitive-object access.
- Column lineage reviewed to catch protected data copied into unprotected objects.
- Policy assignments reviewed periodically via POLICY_REFERENCES.
Sharing and continuity
- Shared-data policies use CURRENT_ACCOUNT() or database roles, not CURRENT_ROLE().
- Time Travel retention set deliberately per data tier.
- Replication or failover configured where continuity requires it, with edition requirements confirmed.
- Named owners for policy changes, access reviews, and recovery testing.
Practical Recommendation
Sequence this work rather than attempting it at once.
Start with authentication, because it is both the highest-risk gap and the one with an external deadline. Enforce MFA for human users, move service accounts to key pair or OAuth, and establish break-glass access before tightening anything.
Then classify. You cannot protect what you have not found, and classification output drives everything downstream.
Then apply policies - tag-based masking first, so protection follows classification automatically, with row access policies where entitlement is genuinely row-level.
Then prove it. Monitor ACCESS_HISTORY, review lineage for leakage into unprotected objects, and schedule access reviews.
Governance is not a project that completes. It is a set of controls with owners, reviewed on a schedule, and evidenced when asked.
Closing the Series
Across five articles the same principle has recurred. A production Snowflake platform is defined less by the features it uses than by the boundaries it enforces: between environments, between raw and governed data, between workloads, between what is measured and what is assumed, and between who may see data and who may not.
Snowflake makes every one of those boundaries straightforward to implement. It does not decide where they belong. That remains an architectural judgment - and it is the judgment that separates a platform that scales from one that merely grows.
Preparing Snowflake for enterprise or regulated use? Armely designs and implements Snowflake governance frameworks covering authentication, data protection, classification, and auditability. Explore Armely's data services or contact our team.
Continue the Series
Previous: Snowflake Performance and Cost Optimization in Practice
This article concludes the Snowflake in Production series.
Technical review sources: