Skip to main content
AWS·10 min read

AWS S3 Bucket Policies — 12 Examples That Actually Lock Down Your Data

Production-ready S3 bucket policy examples for common scenarios. Covers public read access, cross-account access, IP restrictions, VPC endpoints, enforcing encryption, and denying unencrypted uploads.

DT

DevOps Engineer & Technical Writer

The Problem

S3 bucket policies are JSON documents that control who can access your buckets and objects. The syntax is confusing, the interaction with IAM policies creates unexpected behavior, and a misconfigured policy can either lock you out of your own bucket or expose sensitive data to the internet.

This guide provides copy-paste-ready bucket policies for the scenarios you will encounter in production.

S3 Access Control Evaluation Flow Request S3 API Call Bucket Policy Explicit Deny? Highest Priority IAM Policy Allow/Deny? Identity-based VPC Endpoint Policy Network Filter Access Granted Access Denied Any layer can deny — Explicit Deny always wins Deny Check Allow Evaluation Flow

How Bucket Policies Work

A bucket policy evaluates alongside IAM policies. The rules:

  • An explicit Deny always wins, regardless of any Allow
  • If there is no explicit Deny, an explicit Allow grants access
  • If there is neither, access is implicitly denied (default)

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "DescriptiveName",

"Effect": "Allow or Deny",

"Principal": "Who",

"Action": "What operations",

"Resource": "Which objects",

"Condition": {}

}

]

}

Public Read Access (Static Website Hosting)

Allow anyone to read objects — use for static websites and CDN origins:

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "PublicReadGetObject",

"Effect": "Allow",

"Principal": "*",

"Action": "s3:GetObject",

"Resource": "arn:aws:s3:::my-website-bucket/*"

}

]

}

You must also disable S3 Block Public Access settings:

aws s3api put-public-access-block \

--bucket my-website-bucket \

--public-access-block-configuration \

BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false

Public read for specific prefix only

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "PublicReadForAssets",

"Effect": "Allow",

"Principal": "*",

"Action": "s3:GetObject",

"Resource": "arn:aws:s3:::my-bucket/public/*"

}

]

}

Cross-Account Access

Allow another AWS account to read objects

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "CrossAccountRead",

"Effect": "Allow",

"Principal": {

"AWS": "arn:aws:iam::111222333444:root"

},

"Action": [

"s3:GetObject",

"s3:ListBucket"

],

"Resource": [

"arn:aws:s3:::my-shared-bucket",

"arn:aws:s3:::my-shared-bucket/*"

]

}

]

}

Allow a specific role in another account

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "CrossAccountRoleAccess",

"Effect": "Allow",

"Principal": {

"AWS": "arn:aws:iam::111222333444:role/DataPipelineRole"

},

"Action": [

"s3:GetObject",

"s3:PutObject",

"s3:ListBucket"

],

"Resource": [

"arn:aws:s3:::shared-data-bucket",

"arn:aws:s3:::shared-data-bucket/partner-uploads/*"

]

}

]

}

Cross-account with bucket owner enforced

When another account uploads objects, they own those objects by default. Force bucket owner ownership:

aws s3api put-bucket-ownership-controls \

--bucket shared-data-bucket \

--ownership-controls Rules=[{ObjectOwnership=BucketOwnerEnforced}]

IP-Based Restrictions

Allow access only from your office/VPN IPs

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "AllowFromOfficeOnly",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:*",

"Resource": [

"arn:aws:s3:::confidential-bucket",

"arn:aws:s3:::confidential-bucket/*"

],

"Condition": {

"NotIpAddress": {

"aws:SourceIp": [

"203.0.113.0/24",

"198.51.100.0/24"

]

},

"StringNotEquals": {

"aws:PrincipalServiceName": "cloudtrail.amazonaws.com"

}

}

}

]

}

Note the Deny with NotIpAddress pattern — this denies all requests NOT from the specified IPs.

Allow from specific VPC endpoint only

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "DenyNonVPCAccess",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:*",

"Resource": [

"arn:aws:s3:::internal-bucket",

"arn:aws:s3:::internal-bucket/*"

],

"Condition": {

"StringNotEquals": {

"aws:sourceVpce": "vpce-0abc123def456"

}

}

}

]

}

Enforce Encryption

Deny unencrypted uploads (require SSE-S3)

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "DenyUnencryptedUploads",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:PutObject",

"Resource": "arn:aws:s3:::secure-bucket/*",

"Condition": {

"StringNotEquals": {

"s3:x-amz-server-side-encryption": "AES256"

}

}

},

{

"Sid": "DenyNoEncryptionHeader",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:PutObject",

"Resource": "arn:aws:s3:::secure-bucket/*",

"Condition": {

"Null": {

"s3:x-amz-server-side-encryption": "true"

}

}

}

]

}

Require KMS encryption

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "RequireKMSEncryption",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:PutObject",

"Resource": "arn:aws:s3:::sensitive-bucket/*",

"Condition": {

"StringNotEquals": {

"s3:x-amz-server-side-encryption": "aws:kms"

}

}

}

]

}

Enforce HTTPS Only

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "DenyInsecureTransport",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:*",

"Resource": [

"arn:aws:s3:::my-bucket",

"arn:aws:s3:::my-bucket/*"

],

"Condition": {

"Bool": {

"aws:SecureTransport": "false"

}

}

}

]

}

CloudFront Origin Access

Allow only CloudFront to read from your S3 bucket:

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "AllowCloudFrontServicePrincipal",

"Effect": "Allow",

"Principal": {

"Service": "cloudfront.amazonaws.com"

},

"Action": "s3:GetObject",

"Resource": "arn:aws:s3:::my-website-bucket/*",

"Condition": {

"StringEquals": {

"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"

}

}

}

]

}

Combined Production Policy

A production bucket typically combines several restrictions:

{

"Version": "2012-10-17",

"Statement": [

{

"Sid": "EnforceHTTPS",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:*",

"Resource": [

"arn:aws:s3:::prod-data-bucket",

"arn:aws:s3:::prod-data-bucket/*"

],

"Condition": {

"Bool": {

"aws:SecureTransport": "false"

}

}

},

{

"Sid": "EnforceEncryption",

"Effect": "Deny",

"Principal": "*",

"Action": "s3:PutObject",

"Resource": "arn:aws:s3:::prod-data-bucket/*",

"Condition": {

"Null": {

"s3:x-amz-server-side-encryption": "true"

}

}

},

{

"Sid": "AllowDataTeam",

"Effect": "Allow",

"Principal": {

"AWS": "arn:aws:iam::123456789012:role/DataEngineerRole"

},

"Action": [

"s3:GetObject",

"s3:PutObject",

"s3:ListBucket"

],

"Resource": [

"arn:aws:s3:::prod-data-bucket",

"arn:aws:s3:::prod-data-bucket/*"

]

}

]

}

Applying Bucket Policies

# Apply a policy from a file

aws s3api put-bucket-policy \

--bucket my-bucket \

--policy file://bucket-policy.json

# View current policy

aws s3api get-bucket-policy --bucket my-bucket | jq '.Policy | fromjson'

# Delete bucket policy

aws s3api delete-bucket-policy --bucket my-bucket

# Validate policy syntax

aws accessanalyzer validate-policy \

--policy-document file://bucket-policy.json \

--policy-type RESOURCE_POLICY

Common Mistakes

  • Forgetting both bucket and object resourcess3:ListBucket needs the bucket ARN (arn:aws:s3:::bucket), while s3:GetObject needs the object ARN (arn:aws:s3:::bucket/*). Missing either causes Access Denied.
  • Locking yourself out — A Deny-all policy with no exceptions locks out everyone including root through bucket policy evaluation. The root account can still delete the policy via the console.
  • Using Principal: "*" with Allow — This makes the bucket publicly accessible. Always pair with a Condition unless you genuinely want public access.
  • IP restrictions blocking AWS services — If you restrict by IP and use services like CloudTrail, Athena, or EMR, those services cannot access the bucket. Add service principal exceptions.
  • Not accounting for s3:ListBucket separately — Listing contents requires s3:ListBucket on the bucket resource. Object operations are on the object resource with /*.
  • Confusing bucket policy with IAM policy — Both can grant access. Access is allowed if either the IAM policy OR bucket policy allows it (assuming no explicit Deny from either).
  • Quick Reference

    ScenarioKey Elements
    Public read<code class="inline-code">Principal: &quot;*&quot;</code>, <code class="inline-code">Action: &quot;s3:GetObject&quot;</code>
    Cross-account<code class="inline-code">Principal: { AWS: &quot;arn:aws:iam::ACCOUNT:root&quot; }</code>
    IP restriction<code class="inline-code">Condition: { NotIpAddress: { aws:SourceIp: [...] } }</code>
    VPC endpoint only<code class="inline-code">Condition: { StringNotEquals: { aws:sourceVpce: &quot;vpce-...&quot; } }</code>
    Enforce encryptionDeny PutObject without <code class="inline-code">s3:x-amz-server-side-encryption</code>
    HTTPS onlyDeny when <code class="inline-code">aws:SecureTransport: &quot;false&quot;</code>
    CloudFront only<code class="inline-code">Principal: { Service: &quot;cloudfront.amazonaws.com&quot; }</code>
    Prevent deleteDeny <code class="inline-code">s3:DeleteObject</code> for all except admin role

    Summary

    S3 bucket policies enforce access control at the resource level. Start with HTTPS enforcement and encryption requirements on every bucket. Add cross-account or public access only where needed, with the most restrictive conditions possible. Always test policies by assuming the target role and verifying access before relying on them in production.

    ---

    Frequently Asked Questions

    What is the difference between S3 bucket policies and IAM policies?

    S3 bucket policies are resource-based policies attached directly to buckets that control access for any principal, including cross-account access. IAM policies are attached to users or roles within a single account. Use bucket policies for public access rules, cross-account sharing, and VPC endpoint restrictions. Use IAM policies for controlling what your own users and services can access.

    How do I make an S3 bucket completely private?

    Enable S3 Block Public Access at both the account and bucket level, remove any bucket policy statements with "Principal": "*", and ensure no ACLs grant public access. Use aws s3api get-bucket-policy-status to verify the bucket is not public. For extra protection, add a bucket policy that explicitly denies all access except from specific IAM roles or VPC endpoints.

    Why is my S3 bucket policy giving "Access Denied"?

    Check that S3 Block Public Access settings are not overriding your policy, verify the Principal ARN is exactly correct, and look for explicit Deny statements that override Allows. Condition keys like aws:SourceVpc, aws:SourceIp, or s3:x-amz-server-side-encryption can silently block requests that don't match. Use CloudTrail to see the exact policy evaluation result.

    How do I restrict S3 access to a specific VPC?

    Add a bucket policy condition using aws:SourceVpce for VPC endpoint access or aws:SourceVpc for VPC-level restriction. Create a VPC Gateway Endpoint for S3 in your VPC, then add a Deny statement in the bucket policy for all requests not originating from your VPC endpoint. This ensures data never traverses the public internet.

    Can I use S3 bucket policies for cross-account access?

    Yes, add a bucket policy statement with the cross-account principal ARN (role or account) and the desired actions. The target account's role must also have IAM permissions to access S3. For cross-account access, both the bucket policy (resource-based) and the IAM policy on the caller must allow the action.

    ---