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/<key>: "Object tag value" s3:RequestObjectTagKeys: "Tags being set" s3:prefix: "Object key prefix" s3:max-keys: "Max keys in ListBucket" s3:object-lock-mode: "GOVERNANCE or COMPLIANCE"
operators: StringEquals: "Exact match" StringNotEquals: "Not equal" StringLike: "Wildcard match (*)" IpAddress: "IP in CIDR" NotIpAddress: "IP not in CIDR" DateGreaterThan: "After date" DateLessThan: "Before date" Bool: "Boolean check" Null: "Key exists/not exists"
Security Best Practices
security_checklist:
block_public_access:
setting: "Block all public access"
how: |
aws s3api put-public-access-block
--bucket my-bucket
--public-access-block-configuration
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
enable_versioning:
purpose: "Protect against accidental deletion"
how: |
aws s3api put-bucket-versioning
--bucket my-bucket
--versioning-configuration Status=Enabled
enable_logging:
purpose: "Audit access"
how: |
aws s3api put-bucket-logging
--bucket my-bucket
--bucket-logging-status '{"LoggingEnabled":{"TargetBucket":"log-bucket","TargetPrefix":"s3-access/"}}'
default_encryption:
purpose: "Encrypt at rest"
how: |
aws s3api put-bucket-encryption
--bucket my-bucket
--server-side-encryption-configuration
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"alias/s3-key"}}]}'
lifecycle_policy: purpose: "Manage object lifecycle" example: "Transition to Glacier after 90 days, delete after 365"
Troubleshooting
common_issues: access_denied: symptoms: "403 AccessDenied error" checks: - "Verify IAM user/role permissions" - "Check bucket policy allows action" - "Verify resource ARN is correct" - "Check for explicit Deny statements" - "Verify bucket block public access settings" debug: | # 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 — проверяй политики регулярно