Introduction
Amazon S3 is one of the most widely used AWS services, and also one of the most common sources of data leaks. Almost every S3 breach in the news comes down to one thing: a misconfigured access policy.
In this article, I'll explain the two main ways of controlling access to S3, IAM policies and bucket policies, show when to use each, and walk through a hands-on example you can try in your own account.
How S3 Access Is Evaluated
Before writing any policy, it helps to know how AWS decides whether a request is allowed:
Everything is denied by default.
An explicit Allow in a relevant policy grants access.
An explicit Deny always wins, no matter what else says Allow.
For requests within the same account, access is granted if either the IAM policy or the bucket policy allows it (and nothing denies it).
Remember this rule: Explicit Deny > Allow > Default Deny.
IAM Policies vs Bucket Policies
Feature IAM Policy Bucket Policy
Attached to IAM user, group, or role The S3 bucket
Answers the question "What can this identity do?" "Who can access this bucket?"
Has a Principal element No Yes
Cross-account access Needs help from the other side Supported directly
Best for Managing permissions for your users and roles Bucket-wide rules, cross-account access, enforcing conditions
Size limit 2 KB (users), 6 KB (groups), 10 KB (roles) 20 KB
Simple way to remember it:
IAM policy = identity-based ("what can I do?")
Bucket policy = resource-based ("who can touch this bucket?")
Hands-On: Secure a Bucket Step by Step
Step 1: Create a Private Bucket
Open the S3 console and choose Create bucket.
Give it a unique name, for example vishal-secure-demo-bucket.
Keep Block all public access turned ON.
Enable Bucket Versioning.
Enable Default encryption (SSE-S3 or SSE-KMS).
Choose Create bucket.
Tip: Block Public Access is your first line of defense. Leave it on unless you have a very specific reason to turn it off.
Step 2: Create an IAM Policy (Least Privilege)
Let's give a user read and write access to only one bucket, nothing more.
json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::vishal-secure-demo-bucket"
},
{
"Sid": "ReadWriteObjects",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::vishal-secure-demo-bucket/*"
}
]
}
Why two statements? s3:ListBucket applies to the bucket itself, while GetObject and PutObject apply to the objects inside it (/*). This is a very common mistake for beginners.
To apply it:
Go to IAM → Policies → Create policy → JSON and paste the policy.
Name it S3-DemoBucket-ReadWrite.
Attach it to a user, group, or (better) a role.
Step 3: Add a Bucket Policy to Enforce HTTPS
Now let's add a rule at the bucket level. This policy denies any request that doesn't use HTTPS:
json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "",
"Action": "s3:",
"Resource": [
"arn:aws:s3:::vishal-secure-demo-bucket",
"arn:aws:s3:::vishal-secure-demo-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}
To apply it: S3 → your bucket → Permissions → Bucket policy → Edit → paste → Save.
Step 4: Restrict Access to a Specific IAM Role
You can also lock the bucket so that only one role can access it. Replace the account ID and role name with your own:
json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowOnlyAppRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/AppServerRole"
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::vishal-secure-demo-bucket/*"
}
]
}
This is useful when an EC2 instance or Lambda function needs access to the bucket. Use roles instead of long-term access keys.
Step 5: Require Encryption on Upload
This policy blocks any upload that isn't encrypted with SSE-KMS:
json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::vishal-secure-demo-bucket/",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
}
]
}
Step 6: Test Your Setup
Try uploading a file with the IAM user: it should succeed.
Try accessing the bucket over plain HTTP: it should be denied.
Try accessing the bucket with a different user that has no permissions: you should see Access Denied.
Use the IAM Policy Simulator or IAM Access Analyzer to validate your policies.
S3 Security Best Practices
Enable Block Public Access at both the bucket and account level.
Follow least privilege. Grant only the actions and resources that are needed.
Use IAM roles, not long-term access keys.
Enforce HTTPS with aws:SecureTransport.
Encrypt data at rest (SSE-S3 or SSE-KMS).
Enable versioning and consider MFA Delete for critical data.
Turn on logging with S3 server access logs and CloudTrail data events.
Use IAM Access Analyzer to find buckets shared outside your account.
Avoid "Principal": "" with Allow unless you really mean public access.
Review policies regularly and remove permissions that are no longer used.
Common Mistakes to Avoid
Using "Action": "s3:" and "Resource": "" in IAM policies.
Forgetting the / on the resource ARN for object-level actions.
Making a bucket public "temporarily" and forgetting about it.
Hardcoding access keys in application code.
Not testing policies before applying them in production.
Conclusion
Securing S3 isn't about picking IAM policies or bucket policies. The strongest setups use both:
IAM policies control what your users and roles can do.
Bucket policies enforce rules at the bucket itself, like HTTPS-only access, encryption, and cross-account permissions.
Block Public Access, encryption, and logging add extra layers of protection.
Start with least privilege, test your policies, and review them regularly. Your data will be much safer for it.
I'm learning and sharing my AWS journey step by step. If this article helped you, leave a comment or a reaction. Let me know which AWS topic you'd like me to cover next!
Connect with me:
LinkedIn: vishalpatil-devops-aws
dev.to: @vishal_patil_aws
GitHub: VishalPatil-DevOps-AWS
Top comments (0)