Based on the SAA-C03 exam guideReport an error
Most S3 security incidents come from misconfiguration. From 2017 to 2023, dozens of large-scale data breaches were caused by "misconfigured S3 buckets." US military classified documents, a major airline's customer data, and the medical records of millions of people were exposed from S3 buckets left open to the public. In many of these cases, the cause was granting Allow in the bucket policy while turning off Block Public Access.
This article follows the internal logic of how S3's five access-control layers are evaluated, and covers the key-management structure and real differences of each encryption method. It also covers the pattern for preventing data exfiltration with a VPC Endpoint policy, and how to leverage S3 Access Points in a multi-tenant environment.
When S3 first launched (2006), access control was just an ACL (Access Control List). It defined in XML who could access an S3 bucket and object. This was AWS's very first access-control mechanism.
When IAM (Identity and Access Management) launched in 2008, a way to attach policies to users and groups was added. S3 now began to support IAM policies too. But IAM policies define who (identity) can do what, while ACLs define whose access is allowed to a specific bucket/object. Using both together made rules conflict or grow complex.
In 2012, the Bucket Policy was added. As a JSON-based resource policy, it allowed more fine-grained conditions (IP, VPC, whether encrypted, etc.).
And after thousands of public-bucket exposure incidents, in 2018 AWS launched Block Public Access (BPA). It was a last-resort safeguard that "forcibly blocks even if a policy or ACL has a Public Allow, once you flip this switch."
This is the historical reason S3 access control today consists of five layers.
When an S3 request comes in, AWS evaluates whether to allow access in the following order.
[ S3 access evaluation layers (evaluation order) ]
1. Block Public Access (BPA)
→ Blocks Public ACLs/policies. If Deny, reject immediately.
2. SCP (Service Control Policy)
→ Organization-level guardrail of AWS Organizations
3. VPC Endpoint Policy
→ For access via a VPC Endpoint
4. IAM Identity Policy
→ The requester's (user/role) policy
5. Bucket Policy (Resource-based Policy)
→ The JSON policy attached to the bucket
6. Object ACL (depending on object ownership settings)
→ Ignored if Bucket Owner Enforced
7. KMS Key Policy (if SSE-KMS)
→ Whether there is access permission to the KMS key
The core evaluation principle: an explicit Deny at any layer means immediate rejection. Only when Allow (or implicit Allow) is confirmed at every layer is access finally granted.
For cross-account access, both layers are required. For account A's EC2 to access account B's S3 bucket, account A's IAM role must allow s3:GetObject, and account B's bucket policy must trust account A's role. Just one of them is not enough.
💡 Related theory: This multi-layered access-control structure implements the Defense in Depth principle of information security. It realizes, layer by layer, the "Principle of Least Privilege" and "Separation of Duties" emphasized in the "Access Control (AC)" control family of NIST SP 800-53. BPA is an implementation of the "Fail-Safe Default" principle that catches accidental misconfiguration at the very end.
BPA consists of four independent switches.
| Option | Role |
|---|---|
BlockPublicAcls | Blocks adding new Public ACLs. Existing Public ACLs may still work. |
IgnorePublicAcls | Ignores existing Public ACLs. Blocks access from current Public ACLs. |
BlockPublicPolicy | Blocks adding/modifying a bucket policy that makes the bucket Public. |
RestrictPublicBuckets | Blocks the effect of an existing Public bucket policy. Nullifies it even if a Public policy already exists. |
Recommended setting: all four true. The default for a new bucket is all true.
If you enable BPA at the account level (the entire AWS account), it applies to all buckets in the account. Even if you disable BPA on an individual bucket, account-level BPA blocks it. It's a hierarchical relationship.
📚 Case study: After the 2019 Capital One data breach, AWS provides the AWS Config rule
s3-bucket-public-access-prohibitedto automatically detect buckets with BPA disabled. This check is also included in AWS Security Hub's "S3 controls" standard. Many enterprises use an AWS Organizations SCP to enforce specific settings ofs3:PutBucketPublicAccessBlockso that BPA can never be disabled.
Encryption is distinguished by who controls the encryption key. The algorithm (AES-256) is the same in all of them.
[ SSE-S3 encryption flow ]
1. Client makes a PutObject request
2. S3 automatically generates a data key (DEK)
3. Encrypts the data with the DEK using AES-256
4. Encrypts the DEK with a master key managed by S3
5. Stores the encrypted DEK together with the encrypted data
6. On a GetObject request, S3 decrypts the DEK, decrypts the data, and returns it
Since 2023, SSE-S3 is applied by default to all new S3 objects. Data at rest is encrypted with no additional configuration. AWS fully manages the keys, so the customer doesn't need to do key management. The downside is there's no key-access audit log, and fine-grained access control via a key policy is not possible.
[ SSE-KMS envelope encryption flow ]
1. On PutObject, S3 calls the KMS API (GenerateDataKey)
2. KMS returns the DEK (plaintext) and the encrypted DEK
3. S3 encrypts the data with the plaintext DEK, then deletes the plaintext DEK from memory
4. Stores the encrypted DEK together with the encrypted data
5. On GetObject, S3 calls the KMS API (Decrypt)
6. KMS decrypts the DEK and returns it
7. S3 decrypts the data with the DEK and returns it
Advantages of SSE-KMS:
Downside: the cost of KMS API calls. Since KMS is called every time an object is written and read, if thousands of S3 requests occur per second, KMS call costs climb too.
S3 Bucket Keys: a feature to reduce this cost. A Bucket Key temporarily caches, at the S3 level, a data encryption key (DEK) generated by KMS, so that instead of calling KMS on every object request, it generates DEKs from the Bucket Key. It can reduce the number of KMS calls by 99%.
🔍 Going deeper: Envelope Encryption is the approach recommended in NIST SP 800-57. A master key (the KMS CMK) encrypts a data key (DEK), and the DEK encrypts the actual data. The master key never leaves KMS. The benefit of this structure is that you can encrypt countless objects with independent DEKs without ever exposing the master key. To rotate the master key, you just re-encrypt the existing DEKs with the new key — there's no need to re-encrypt the data itself.
[ SSE-C flow ]
PUT request headers:
x-amz-server-side-encryption-customer-algorithm: AES256
x-amz-server-side-encryption-customer-key: [Base64-encoded 256-bit key]
x-amz-server-side-encryption-customer-key-MD5: [MD5 of the key]
S3 encrypts the data with the provided key, then immediately discards the key (doesn't store it)
→ decryption is impossible without the same key
With SSE-C, AWS does not store the key. The customer manages the key directly and must include it in the header on every request. Lose the key and you lose the data permanently. HTTPS is required (plaintext transmission is not allowed).
It satisfies regulatory requirements that "the encryption key must not exist on AWS infrastructure," but the key-management burden is heavy. In practice, it's used integrated with an in-house HSM (Hardware Security Module) or key-management system.
The client (SDK, app) encrypts the data and then uploads it to S3. S3 merely receives and stores an already-encrypted Blob. Neither S3 nor AWS can see the plaintext data.
It's the strongest security, but application code complexity is high, and you can't use S3's server-side features (Object Lambda, S3 Select, etc.). It's used in the most heavily regulated environments requiring an HSM at FIPS 140-3 Level 3 or above.
💡 Related theory: The comparison of encryption methods is organized by the concept of the Trust Boundary. SSE-S3 trusts the entire AWS infrastructure. SSE-KMS trusts the AWS infrastructure but restricts key access with IAM + KMS policies. SSE-C trusts the S3 service but never puts the key on AWS. CSE doesn't trust the AWS service itself. This spectrum connects to the Zero Trust security model's principle that "no one is trusted by default."
DSSE-KMS encrypts data twice with KMS keys. It was designed for FIPS 140-3 regulations and certain data requirements of US government agencies. For an ordinary enterprise it's overkill.
Historically, S3 ACLs were used to solve the ownership problem of objects uploaded by a different AWS account. When account B uploaded an object to account A's bucket, for account A to read or delete that object, the ACL had to grant permission to account A.
To simplify this, AWS introduced the Object Ownership setting.
BucketOwnerPreferred: if another account includes the bucket-owner-full-control ACL on upload, the bucket owner owns the object.BucketOwnerEnforced (recommended): fully disables ACLs. The bucket owner owns all objects. Access control is via the bucket policy only.BucketOwnerEnforced removes the legacy ACL mechanism and simplifies access control down to a single bucket policy. AWS strongly recommends this mode.
When multiple teams access a large data-lake bucket, each team should access only its own prefix (/team-a/, /team-b/). Putting every team's rules into a single bucket policy makes the policy too complex, and management gets harder as teams grow.
S3 Access Points are named network endpoints for a bucket. You can attach a policy to each Access Point, and the bucket policy can allow access only through the Access Points.
[ S3 Access Points structure ]
S3 bucket (data-lake)
│
├─ Access Point: team-a-ap
│ Policy: s3:Get* on arn:...data-lake/team-a/*
│ VPC restriction: vpc-aaa
│
├─ Access Point: team-b-ap
│ Policy: s3:Get* s3:Put* on arn:...data-lake/team-b/*
│
└─ Access Point: analytics-ap
Policy: s3:Get* on arn:...data-lake/*
VPC restriction: vpc-analytics
A special type of Access Point, the Multi-Region Access Point, presents buckets in multiple regions as a single global endpoint. Based on Route 53 Anycast, it routes to the nearest region's bucket.
S3 is accessed over the internet by default. When EC2 accesses S3, it also goes through a NAT Gateway or internet gateway. On this path, data can leave the AWS network, and outbound transfer costs are incurred too.
VPC Gateway Endpoint for S3: provides a path to access S3 directly from within the VPC, without the internet. No additional cost. Add S3's Prefix List to the Route Table and S3 traffic automatically uses this path.
Endpoint policy: by attaching a policy to the VPC Endpoint, you can restrict "this VPC can access only specific buckets." It's key to preventing data exfiltration.
{
"Statement": [{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::company-data-bucket",
"arn:aws:s3:::company-data-bucket/*"
]
}]
}Combine this Endpoint policy with a bucket policy that allows access only through a specific VPC Endpoint via the aws:SourceVpce condition, and access to S3 over the internet is blocked.
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": ["arn:aws:s3:::company-data-bucket/*"],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-1234abcd"
}
}
}📚 Case study: Financial services companies often use this pattern to ensure a data-analytics instance accesses only approved data buckets and cannot exfiltrate data to a personal S3 account or an external bucket. Setting the VPC Endpoint policy to "allow only buckets of a specific account" blocks sending data to another account's S3, even within the same AWS service.
CORS (Cross-Origin Resource Sharing) is an HTTP mechanism that governs the browser's Same-Origin Policy. When JavaScript loaded from https://app.example.com accesses a file at https://my-bucket.s3.amazonaws.com, the browser checks the CORS policy.
An S3 bucket's CORS configuration:
[{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT"],
"AllowedOrigins": ["https://app.example.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}]CORS configuration is essential in the direct-browser-upload pattern via a Presigned URL. The browser sends an OPTIONS preflight request before the PUT, and S3 responds whether it's allowed based on the CORS configuration.
S3 static website hosting makes a bucket behave like an HTTP server. But exposing it directly requires disabling BPA, and it doesn't support HTTPS either.
The modern pattern is the CloudFront + OAC (Origin Access Control) combination.
[ CloudFront + OAC architecture ]
User (HTTPS) → CloudFront
├─ cache HIT: respond immediately
└─ cache MISS: OAC-signed request → S3 bucket (Private)
← S3 response → CloudFront cache → user
Bucket policy: allow only the CloudFront OAC Principal
BPA: fully enabled (blocks direct external access)
OAC (Origin Access Control) is the successor to OAI (Origin Access Identity); when CloudFront accesses S3, it sends a request signed with AWS Signature V4. The S3 bucket sets its bucket policy to allow only requests from this specific CloudFront Distribution.
⚠️ Pitfall: OAI (Origin Access Identity) is legacy, and OAC (Origin Access Control) is recommended. OAC also supports S3 buckets encrypted with SSE-KMS and supports S3 in all regions. If an exam question asks about the latest capability, OAC is the right answer.
# Enable all four Block Public Access settings
aws s3api put-public-access-block \
--bucket my-bucket \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,\
BlockPublicPolicy=true,RestrictPublicBuckets=true
# Enable account-level BPA
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,\
BlockPublicPolicy=true,RestrictPublicBuckets=true
# Set SSE-KMS default encryption (with Bucket Keys)
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:ap-northeast-2:123456789012:key/abc123"
},
"BucketKeyEnabled": true
}]
}'
# HTTPS-enforcing bucket policy
aws s3api put-bucket-policy \
--bucket my-bucket \
--policy '{
"Statement": [{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"Bool": {"aws:SecureTransport": "false"}
}
}]
}'
# Object Ownership = BucketOwnerEnforced (disable ACLs)
aws s3api put-bucket-ownership-controls \
--bucket my-bucket \
--ownership-controls '{
"Rules": [{"ObjectOwnership": "BucketOwnerEnforced"}]
}'
# Create an S3 Access Point (VPC restriction)
aws s3control create-access-point \
--account-id 123456789012 \
--name team-a-access-point \
--bucket my-data-lake \
--vpc-configuration VpcId=vpc-12345678
# Set the Access Point policy
aws s3control put-access-point-policy \
--account-id 123456789012 \
--name team-a-access-point \
--policy '{
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/team-a-role"},
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:ap-northeast-2:123456789012:accesspoint/team-a-access-point/object/team-a/*"
}]
}'S3 security is not a single setting but layered Defense in Depth. BPA blocks accidental public exposure, IAM + bucket policy controls normal access, the VPC Endpoint policy restricts the network path, and KMS protects the data itself.
For encryption, "who controls the key" is the crux. If you need an audit trail and fine-grained key access control, SSE-KMS; if you need cost optimization, Bucket Keys; if you don't want to put the key on AWS, SSE-C; if you want full control, CSE.
Click a choice to reveal the answer and explanation.
Question 1
A security audit team must audit all data access on an S3 bucket (who read which object and when). For compliance, automatic annual rotation of the encryption key is also required. What is the most suitable encryption method?
Question 2
A company's central data-lake S3 bucket is accessed by a data-analytics team (prefix: /analytics/), a marketing team (prefix: /marketing/), and an HR team (prefix: /hr/). Each team should access only its own prefix, and access paths should be managed independently per team. How do you keep the bucket policy simple?
Question 3
A data-analytics EC2 instance reads sensitive data from an S3 bucket. The security team wants to prevent data exfiltration over the internet and stop this instance from accessing any S3 outside the approved bucket. How do you configure it?
Question 4
A dev team hosts a React app in an S3 bucket and distributes it via CloudFront. To block direct (public) access to the S3 bucket and allow access only through CloudFront, what do you do?
Question 5
A company wants to allow only HTTPS on all S3 requests and block access over HTTP. How do you configure it?
Question 6
There is an analytics workload reading more than 10,000 objects per second from an SSE-KMS-encrypted S3 bucket. KMS API call costs are higher than expected. How do you reduce the KMS cost while keeping encryption?
Question 7
Select 4 answersSelect all cases below where an "explicit Deny" occurs in S3 access evaluation.