Home / Data Platform / Warehouses

Data PlatformTroubleshootingAdvanced

Snowflake external stage access denied: fixing storage integrations for Amazon S3 and Azure Blob Storage

LIST or COPY INTO on an external stage fails with access denied or "not allowed by integration". Check the integration, the IAM trust or Azure consent, the role, and the storage firewall.

Deepak Bhardwaj8 min read

Applies toSnowflake storage integrations and external stages on Amazon S3 and Azure Blob Storage / ADLS Gen2

Symptom

A load that worked yesterday, or a new stage you just created, fails as soon as Snowflake touches the bucket or container:

  • LIST @stage or COPY INTO returns an access denied error from S3 (Access Denied (Status Code: 403; Error Code: AccessDenied)) or Azure (This request is not authorized to perform this operation).
  • CREATE STAGE fails because the location is not allowed by the storage integration.
  • CREATE STAGE fails with insufficient privileges on the integration.
  • Snowpipe stops loading with no obvious error in the pipe.

Error text varies between Snowflake releases and cloud providers, so match on the general meaning, not the exact string.

A storage integration has four parts that must all agree:

  1. The integration object in Snowflake (allowed locations, role ARN or tenant ID).
  2. The trust on the cloud side: an IAM role trust policy on AWS, or admin consent plus a role assignment on Azure.
  3. Snowflake privileges: the role creating or using the stage needs USAGE on the integration.
  4. The network path: bucket policies, storage firewalls and private endpoints.

Work through them in that order.

Step 1: test the integration directly

SYSTEM$VALIDATE_STORAGE_INTEGRATION tests the integration without a stage, so it separates integration problems from stage problems.

USE ROLE ACCOUNTADMIN;

SELECT SYSTEM$VALIDATE_STORAGE_INTEGRATION(
  'S3_RAW_INT',                        -- integration name, case-sensitive
  's3://acme-raw/landing/',            -- must be inside STORAGE_ALLOWED_LOCATIONS
  'snowflake_validate.txt',            -- test file
  'list');                             -- read | write | list | delete | all

The result is JSON with an overall status and a status per action. read fails if the test file does not exist, and write fails if it already does, so pick the action you need. all writes and deletes a test object, so only use it where Snowflake is meant to have write access.

Result Where to look
Path not allowed Step 2: allowed locations
AssumeRole / authorization failure on AWS Step 3: trust policy and external ID
Authorization failure on Azure Step 4: consent and role assignment
Success, but the stage still fails Step 5: stage definition and grants
Success from some places, timeouts or 403 from others Step 6: network rules

Step 2: the path must be in the allowed locations

DESC STORAGE INTEGRATION S3_RAW_INT;

Check ENABLED, STORAGE_ALLOWED_LOCATIONS and STORAGE_BLOCKED_LOCATIONS. The stage URL must start with one of the allowed prefixes and not match a blocked one. Common mistakes:

  • Missing trailing slash, or a different path case. S3 keys and Azure paths are case-sensitive.
  • Azure stage written as azure://acct.blob.core.windows.net/container/ while the integration allows a different account or container.
  • A new folder added by a project team that was never added to the integration.

ALTER ... SET STORAGE_ALLOWED_LOCATIONS replaces the whole list. Copy the current value from DESC and append to it:

ALTER STORAGE INTEGRATION S3_RAW_INT SET
  STORAGE_ALLOWED_LOCATIONS = ('s3://acme-raw/landing/', 's3://acme-raw/partner-feeds/');

Warning

Leaving out an existing path in the SET breaks every stage and pipe that uses it, without warning. Save the DESC output before changing it.

Step 3: AWS – trust policy and external ID

From DESC STORAGE INTEGRATION, note STORAGE_AWS_ROLE_ARN, STORAGE_AWS_IAM_USER_ARN and STORAGE_AWS_EXTERNAL_ID. Compare them with the role in AWS:

aws iam get-role --role-name snowflake-raw-reader \
  --query 'Role.AssumeRolePolicyDocument' --output json

The trust policy must allow sts:AssumeRole for the Snowflake IAM user ARN, with a condition on the external ID:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "<STORAGE_AWS_IAM_USER_ARN>" },
    "Action": "sts:AssumeRole",
    "Condition": { "StringEquals": { "sts:ExternalId": "<STORAGE_AWS_EXTERNAL_ID>" } }
  }]
}
aws iam update-assume-role-policy --role-name snowflake-raw-reader \
  --policy-document file://trust.json

The most common cause of "it worked yesterday" is that someone ran CREATE OR REPLACE STORAGE INTEGRATION. Unless you set STORAGE_AWS_EXTERNAL_ID explicitly, Snowflake generates a new external ID for the new integration, and the old trust policy no longer matches. Either update the trust policy with the new value from DESC, or recreate the integration with a fixed external ID:

CREATE OR REPLACE STORAGE INTEGRATION S3_RAW_INT
  TYPE = EXTERNAL_STAGE
  STORAGE_PROVIDER = 'S3'
  ENABLED = TRUE
  STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::111122223333:role/snowflake-raw-reader'
  STORAGE_AWS_EXTERNAL_ID = 'acme_raw_ext_id'
  STORAGE_ALLOWED_LOCATIONS = ('s3://acme-raw/landing/');

Caution

CREATE OR REPLACE on an integration in use breaks every stage and pipe that references it until the trust is fixed. Prefer ALTER STORAGE INTEGRATION for changes, and check the stages that depend on it first.

If the trust is correct and access still fails, check the role's permission policy (s3:GetObject, s3:GetObjectVersion, s3:ListBucket for reads; s3:PutObject and s3:DeleteObject for unload or purge), the bucket policy, and any KMS key policy. With SSE-KMS the role also needs kms:Decrypt on the key, and an explicit deny in the bucket policy overrides everything.

DESC STORAGE INTEGRATION on an Azure integration shows AZURE_TENANT_ID, AZURE_CONSENT_URL and AZURE_MULTI_TENANT_APP_NAME.

  1. AZURE_TENANT_ID must be the tenant that owns the storage account. A wrong tenant ID is a common mistake when there is more than one tenant.
  2. A tenant admin must have opened AZURE_CONSENT_URL and accepted. That creates a service principal for Snowflake in your tenant.
  3. That service principal needs a data-plane role on the storage account or container. Owner or Contributor alone is not enough; those are management roles.

The service principal display name is the part of AZURE_MULTI_TENANT_APP_NAME before the underscore:

az ad sp list --display-name <app-name-prefix> --query "[].{name:displayName, id:id}" -o table

az role assignment list --assignee <sp-object-id> --all -o table

az role assignment create --assignee-object-id <sp-object-id> \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Reader" \
  --scope "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<acct>/blobServices/default/containers/<container>"

Use Storage Blob Data Contributor if Snowflake needs to unload, write or PURGE. New role assignments can take several minutes to take effect, so wait before retesting.

Step 5: stage definition and Snowflake grants

If validation succeeds but the stage fails, compare the two:

DESC STAGE raw.landing.sales_stage;   -- check URL and STORAGE_INTEGRATION
SHOW GRANTS ON INTEGRATION S3_RAW_INT;
  • The stage URL must sit under an allowed location, and the stage must reference the integration you tested.
  • The role that creates the stage needs USAGE on the integration and CREATE STAGE on the schema. The role that loads needs USAGE on the stage plus the table privileges.
GRANT USAGE ON INTEGRATION S3_RAW_INT TO ROLE LOADER_ROLE;
GRANT CREATE STAGE ON SCHEMA raw.landing TO ROLE LOADER_ROLE;

Step 6: network rules on the storage side

Storage firewalls cause errors that look the same as permission errors.

On Azure, if the storage account allows only selected networks, add the Snowflake VNet subnet IDs for your account's region:

SELECT SYSTEM$GET_SNOWFLAKE_PLATFORM_INFO();   -- returns snowflake-vnet-subnet-id values
az storage account network-rule add -g <rg> --account-name <acct> --subnet <snowflake-subnet-id>

On AWS, check for bucket policy conditions such as aws:SourceVpce or aws:SourceIp that do not include Snowflake's traffic. If you use private connectivity to storage, the setup is different; follow the Snowflake private connectivity documentation for your edition and cloud rather than opening the firewall.

Symptom → cause → fix

Symptom Cause Fix
Location not allowed by integration Path outside STORAGE_ALLOWED_LOCATIONS ALTER ... SET with the full list plus the new path
AWS 403 after an integration change CREATE OR REPLACE generated a new external ID Update trust policy, or set STORAGE_AWS_EXTERNAL_ID
AWS 403 only on some files KMS key policy or a bucket policy deny Grant kms:Decrypt; review deny statements
Azure "not authorized" No consent, wrong tenant, or no Blob Data role Consent URL, fix tenant ID, assign data role
Azure works from the portal but not Snowflake Storage firewall Add Snowflake subnet IDs
Insufficient privileges on integration Role lacks USAGE GRANT USAGE ON INTEGRATION

Prevent it next time

  • Manage integrations with ALTER, not CREATE OR REPLACE, and set STORAGE_AWS_EXTERNAL_ID explicitly so a rebuild keeps the trust.
  • Keep the integration DDL, IAM trust policy and Azure role assignments in the same repository so a reviewer sees both sides of a change.
  • Run SYSTEM$VALIDATE_STORAGE_INTEGRATION with list from a scheduled task or monitoring job and alert on failure, before the nightly load finds the problem.
  • Record which stages and pipes depend on each integration, and check that list before any change.