AWS S3 Bucket Policy Expert
Expert guidance on creating, analyzing, and optimizing AWS S3 bucket policies with focus on security, access control, and compliance.
Policy Structure
{
"Version"
:
"2012-10-17"
,
"Id"
:
"PolicyIdentifier"
,
"Statement"
:
[
{
"Sid"
:
"StatementIdentifier"
,
"Effect"
:
"Allow | Deny"
,
"Principal"
:
{
"AWS"
:
"arn:aws:iam::account-id:root"
}
,
"Action"
:
[
"s3:GetObject"
,
"s3:PutObject"
]
,
"Resource"
:
[
"arn:aws:s3:::bucket-name"
,
"arn:aws:s3:::bucket-name/"
]
,
"Condition"
:
{
"StringEquals"
:
{
"s3:x-amz-acl"
:
"bucket-owner-full-control"
}
}
}
]
}
Core Principles
security_principles
:
least_privilege
:
description
:
"Grant only minimum necessary permissions"
practice
:
"Start with deny all, add specific allows"
explicit_deny
:
description
:
"Deny always overrides Allow"
practice
:
"Use Deny for security guardrails"
defense_in_depth
:
description
:
"Multiple layers of security"
practice
:
"Combine bucket policy + IAM + ACL + encryption"
avoid_wildcards
:
bad
:
'"Principal": ""'
better
:
'"Principal": {"AWS": "arn:aws:iam::123456789012:root"}'
common_mistakes
:
-
"Using Principal: * without conditions"
-
"Missing resource ARN for objects (/)"
-
"Forgetting to block public access"
-
"Not enabling versioning before policies"
Common Policy Patterns
Public Read for Static Website
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"PublicReadGetObject"
,
"Effect"
:
"Allow"
,
"Principal"
:
""
,
"Action"
:
"s3:GetObject"
,
"Resource"
:
"arn:aws:s3:::my-website-bucket/"
,
"Condition"
:
{
"StringEquals"
:
{
"s3:ExistingObjectTag/public"
:
"true"
}
}
}
]
}
Cross-Account Access
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"CrossAccountAccess"
,
"Effect"
:
"Allow"
,
"Principal"
:
{
"AWS"
:
"arn:aws:iam::987654321098:root"
}
,
"Action"
:
[
"s3:GetObject"
,
"s3:PutObject"
,
"s3:ListBucket"
]
,
"Resource"
:
[
"arn:aws:s3:::shared-bucket"
,
"arn:aws:s3:::shared-bucket/"
]
,
"Condition"
:
{
"StringEquals"
:
{
"s3:x-amz-acl"
:
"bucket-owner-full-control"
}
}
}
]
}
CloudFront Origin Access Control
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"AllowCloudFrontServicePrincipal"
,
"Effect"
:
"Allow"
,
"Principal"
:
{
"Service"
:
"cloudfront.amazonaws.com"
}
,
"Action"
:
"s3:GetObject"
,
"Resource"
:
"arn:aws:s3:::my-cdn-bucket/"
,
"Condition"
:
{
"StringEquals"
:
{
"AWS:SourceArn"
:
"arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}
Enforce Encryption
{
"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"
:
"aws:kms"
}
}
}
,
{
"Sid"
:
"DenyIncorrectKMSKey"
,
"Effect"
:
"Deny"
,
"Principal"
:
""
,
"Action"
:
"s3:PutObject"
,
"Resource"
:
"arn:aws:s3:::secure-bucket/"
,
"Condition"
:
{
"StringNotEquals"
:
{
"s3:x-amz-server-side-encryption-aws-kms-key-id"
:
"arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
}
}
}
]
}
IP-Based Restrictions
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"AllowFromCorporateNetwork"
,
"Effect"
:
"Allow"
,
"Principal"
:
""
,
"Action"
:
[
"s3:GetObject"
,
"s3:PutObject"
]
,
"Resource"
:
"arn:aws:s3:::internal-bucket/"
,
"Condition"
:
{
"IpAddress"
:
{
"aws:SourceIp"
:
[
"192.0.2.0/24"
,
"203.0.113.0/24"
]
}
}
}
,
{
"Sid"
:
"DenyFromOtherIPs"
,
"Effect"
:
"Deny"
,
"Principal"
:
""
,
"Action"
:
"s3:"
,
"Resource"
:
[
"arn:aws:s3:::internal-bucket"
,
"arn:aws:s3:::internal-bucket/"
]
,
"Condition"
:
{
"NotIpAddress"
:
{
"aws:SourceIp"
:
[
"192.0.2.0/24"
,
"203.0.113.0/24"
]
}
}
}
]
}
VPC Endpoint Access Only
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"DenyNonVPCAccess"
,
"Effect"
:
"Deny"
,
"Principal"
:
""
,
"Action"
:
"s3:"
,
"Resource"
:
[
"arn:aws:s3:::private-bucket"
,
"arn:aws:s3:::private-bucket/"
]
,
"Condition"
:
{
"StringNotEquals"
:
{
"aws:SourceVpce"
:
"vpce-1234567890abcdef0"
}
}
}
]
}
MFA Delete Protection
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"RequireMFAForDelete"
,
"Effect"
:
"Deny"
,
"Principal"
:
""
,
"Action"
:
[
"s3:DeleteObject"
,
"s3:DeleteObjectVersion"
]
,
"Resource"
:
"arn:aws:s3:::critical-bucket/"
,
"Condition"
:
{
"Bool"
:
{
"aws:MultiFactorAuthPresent"
:
"false"
}
}
}
]
}
Time-Based Access
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"BusinessHoursOnly"
,
"Effect"
:
"Deny"
,
"Principal"
:
""
,
"Action"
:
"s3:"
,
"Resource"
:
[
"arn:aws:s3:::business-bucket"
,
"arn:aws:s3:::business-bucket/"
]
,
"Condition"
:
{
"DateGreaterThan"
:
{
"aws:CurrentTime"
:
"2024-01-01T18:00:00Z"
}
,
"DateLessThan"
:
{
"aws:CurrentTime"
:
"2024-01-02T09:00:00Z"
}
}
}
]
}
CloudTrail Logging
{
"Version"
:
"2012-10-17"
,
"Statement"
:
[
{
"Sid"
:
"AWSCloudTrailAclCheck"
,
"Effect"
:
"Allow"
,
"Principal"
:
{
"Service"
:
"cloudtrail.amazonaws.com"
}
,
"Action"
:
"s3:GetBucketAcl"
,
"Resource"
:
"arn:aws:s3:::cloudtrail-logs-bucket"
,
"Condition"
:
{
"StringEquals"
:
{
"AWS:SourceArn"
:
"arn:aws:cloudtrail:us-east-1:123456789012:trail/mytrail"
}
}
}
,
{
"Sid"
:
"AWSCloudTrailWrite"
,
"Effect"
:
"Allow"
,
"Principal"
:
{
"Service"
:
"cloudtrail.amazonaws.com"
}
,
"Action"
:
"s3:PutObject"
,
"Resource"
:
"arn:aws:s3:::cloudtrail-logs-bucket/AWSLogs/123456789012/"
,
"Condition"
:
{
"StringEquals"
:
{
"s3:x-amz-acl"
:
"bucket-owner-full-control"
,
"AWS:SourceArn"
:
"arn:aws:cloudtrail:us-east-1:123456789012:trail/mytrail"
}
}
}
]
}
Condition Keys Reference
condition_keys
:
global
:
aws:SourceIp
:
"IP address or CIDR"
aws:SourceVpc
:
"VPC ID"
aws:SourceVpce
:
"VPC endpoint ID"
aws:PrincipalOrgID
:
"AWS Organization ID"
aws:CurrentTime
:
"ISO 8601 datetime"
aws:MultiFactorAuthPresent
:
"true/false"
aws:SecureTransport
:
"true/false"
s3_specific
:
s3:x-amz-acl
:
"ACL to apply"
s3:x-amz-server-side-encryption
:
"AES256 or aws:kms"
s3:x-amz-server-side-encryption-aws-kms-key-id
:
"KMS key ARN"
s3:ExistingObjectTag/
Check effective policy
aws s3api get-bucket-policy --bucket my-bucket
Test access
aws s3api head
object
- bucket my - bucket - - key test.txt policy_too_large : limit : "20 KB maximum" solutions : - "Use IAM policies instead" - "Consolidate statements" - "Use conditions instead of listing principals" - "Reference IAM roles instead of users" invalid_principal : symptoms : "MalformedPolicy error" common_causes : - "Account ID doesn't exist" - "Role/user doesn't exist" - "Typo in ARN format" format : "arn:aws:iam::ACCOUNT-ID:root/role/user" condition_not_working : checks : - "Verify condition key spelling" - "Check operator type matches value type" - "Ensure condition applies to correct action" Policy Validation
Validate policy syntax
aws iam simulate-custom-policy \ --policy-input-list file://policy.json \ --action-names s3:GetObject \ --resource-arns arn:aws:s3:::my-bucket/test.txt
Test policy with IAM Policy Simulator
Console: https://policysim.aws.amazon.com/
Check for public access
aws s3api get-bucket-policy-status --bucket my-bucket
List bucket policies
aws s3api get-bucket-policy --bucket my-bucket --output text Terraform Example resource "aws_s3_bucket" "example" { bucket = "my-secure-bucket" } resource "aws_s3_bucket_public_access_block" "example" { bucket = aws_s3_bucket.example.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_policy" "example" { bucket = aws_s3_bucket.example.id policy = jsonencode( { Version = "2012-10-17" Statement = [ { Sid = "EnforceSSL" Effect = "Deny" Principal = "" Action = "s3:" Resource = [ aws_s3_bucket.example.arn, " $ { aws_s3_bucket . example . arn } /*" ] Condition = { Bool = { "aws:SecureTransport" = "false" } } } ] } ) } Лучшие практики Least privilege — минимальные необходимые права Block public access — блокируй публичный доступ по умолчанию Use conditions — добавляй условия для дополнительной защиты Enable logging — логируй все обращения к bucket Version control — храни политики в git Regular audits — проверяй политики регулярно