| name | s3-bucket-policy |
| description | Эксперт по S3 политикам. Используй для IAM policies, bucket permissions, cross-account access и security best practices. |
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 — проверяй политики регулярно