Skip to content
Codeloom
AWS

AWS S3 Security: Policies, Encryption, and Access Control

Secure your AWS S3 buckets with bucket policies, encryption at rest and in transit, access logging, and IAM-based access control best practices.

·7 min read · By Codeloom
Intermediate 17 min read

What you'll learn

  • Configure S3 bucket policies and ACLs correctly
  • Enable server-side encryption with SSE-S3, SSE-KMS, and SSE-C
  • Block public access and enforce HTTPS-only connections
  • Set up access logging and monitor bucket activity

Prerequisites

  • AWS account with S3 access
  • AWS CLI installed and configured
  • Basic understanding of IAM policies

Amazon S3 is one of the most widely used AWS services, storing everything from application assets to sensitive customer data. Its flexibility is also its risk — a single misconfigured bucket policy can expose terabytes of private data to the internet. This guide covers the layers of S3 security you should implement to protect your data.

Block Public Access First

AWS provides account-level and bucket-level public access blocks. Enable both as your first line of defense. These settings override any bucket policy or ACL that would otherwise grant public access.

# Block public access at the account level
aws s3control put-public-access-block \
  --account-id 123456789012 \
  --public-access-block-configuration \
    BlockPublicAcls=true,\
    IgnorePublicAcls=true,\
    BlockPublicPolicy=true,\
    RestrictPublicBuckets=true

# Block public access at the bucket level
aws s3api put-public-access-block \
  --bucket my-secure-bucket \
  --public-access-block-configuration \
    BlockPublicAcls=true,\
    IgnorePublicAcls=true,\
    BlockPublicPolicy=true,\
    RestrictPublicBuckets=true
# CloudFormation
Resources:
  SecureBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-secure-bucket
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced

Setting ObjectOwnership to BucketOwnerEnforced disables ACLs entirely, which is the recommended approach. ACLs are a legacy access control mechanism and bucket policies provide far more granular control.

Writing Secure Bucket Policies

Bucket policies are JSON documents that define who can do what with your bucket and its objects. A well-crafted bucket policy is your primary access control tool.

Enforce HTTPS Only

Deny all requests that do not use TLS encryption in transit.

aws s3api put-bucket-policy --bucket my-secure-bucket --policy '{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    }
  ]
}'

Enforce Minimum TLS Version

Go beyond just requiring HTTPS by mandating TLS 1.2 or later.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceTLS12",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::my-secure-bucket",
        "arn:aws:s3:::my-secure-bucket/*"
      ],
      "Condition": {
        "NumericLessThan": {
          "s3:TlsVersion": 1.2
        }
      }
    }
  ]
}

Restrict Access to Specific VPCs

For internal data buckets, restrict access to requests originating from your VPC.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VPCOnlyAccess",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::internal-data-bucket",
        "arn:aws:s3:::internal-data-bucket/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:SourceVpce": "vpce-1a2b3c4d"
        }
      }
    }
  ]
}

Server-Side Encryption

S3 offers three server-side encryption options. Since January 2023, all new objects are automatically encrypted with SSE-S3, but you should understand when to use stronger options.

SSE-S3 (Default)

Amazon manages the keys entirely. This is the simplest option and is now enabled by default.

Resources:
  DefaultEncryptionBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

You control the encryption key through AWS KMS, gaining audit trails, key rotation, and granular access control.

# Create a KMS key for S3 encryption
aws kms create-key \
  --description "S3 bucket encryption key" \
  --key-usage ENCRYPT_DECRYPT \
  --key-spec SYMMETRIC_DEFAULT

# Configure the bucket to use SSE-KMS
aws s3api put-bucket-encryption \
  --bucket my-secure-bucket \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "aws:kms",
          "KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789:key/my-key-id"
        },
        "BucketKeyEnabled": true
      }
    ]
  }'

Enabling BucketKeyEnabled reduces KMS API calls by generating a bucket-level key, which can significantly reduce costs at scale.

Enforce Encryption on Upload

Deny any PutObject request that does not include the correct encryption header.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-secure-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    }
  ]
}

IAM Access Control Patterns

Cross-Account Access with IAM Roles

When another AWS account needs access to your bucket, use IAM roles rather than bucket policies with account principals.

Resources:
  CrossAccountRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: s3-cross-account-reader
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: 'arn:aws:iam::987654321098:root'
            Action: 'sts:AssumeRole'
            Condition:
              StringEquals:
                sts:ExternalId: 'unique-external-id-here'
      Policies:
        - PolicyName: S3ReadAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:ListBucket
                Resource:
                  - 'arn:aws:s3:::shared-data-bucket'
                  - 'arn:aws:s3:::shared-data-bucket/*'

S3 Access Points for Shared Datasets

When multiple teams or applications need different levels of access to the same bucket, use S3 Access Points instead of complex bucket policies.

# Create an access point for the analytics team
aws s3control create-access-point \
  --account-id 123456789012 \
  --name analytics-ap \
  --bucket my-data-bucket \
  --vpc-configuration VpcId=vpc-1a2b3c4d

# Set the access point policy
aws s3control put-access-point-policy \
  --account-id 123456789012 \
  --name analytics-ap \
  --policy '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "AWS": "arn:aws:iam::123456789012:role/analytics-role"
        },
        "Action": ["s3:GetObject", "s3:ListBucket"],
        "Resource": [
          "arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-ap",
          "arn:aws:s3:us-east-1:123456789012:accesspoint/analytics-ap/object/*"
        ]
      }
    ]
  }'

Access Logging and Monitoring

Enable Server Access Logging

S3 server access logs capture detailed records of every request made to your bucket.

Resources:
  LoggingBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-access-logs-bucket
      LifecycleConfiguration:
        Rules:
          - Id: DeleteOldLogs
            Status: Enabled
            ExpirationInDays: 90

  DataBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-data-bucket
      LoggingConfiguration:
        DestinationBucketName: !Ref LoggingBucket
        LogFilePrefix: s3-access-logs/

CloudTrail Data Events

For compliance-critical buckets, enable CloudTrail data events to get API-level audit logs.

aws cloudtrail put-event-selectors \
  --trail-name my-trail \
  --event-selectors '[
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": true,
      "DataResources": [
        {
          "Type": "AWS::S3::Object",
          "Values": ["arn:aws:s3:::my-secure-bucket/"]
        }
      ]
    }
  ]'

EventBridge Notifications for Security Alerts

Trigger alerts when bucket configurations change or when objects are accessed unexpectedly.

Resources:
  S3SecurityRule:
    Type: AWS::Events::Rule
    Properties:
      Description: Alert on S3 bucket policy changes
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - AWS API Call via CloudTrail
        detail:
          eventSource:
            - s3.amazonaws.com
          eventName:
            - PutBucketPolicy
            - DeleteBucketPolicy
            - PutBucketAcl
            - PutPublicAccessBlock
      Targets:
        - Arn: !Ref SecurityAlertTopic
          Id: SecurityAlert

Versioning and Object Lock

Enable Versioning for Data Protection

Versioning protects against accidental deletions and overwrites by keeping all versions of an object.

aws s3api put-bucket-versioning \
  --bucket my-secure-bucket \
  --versioning-configuration Status=Enabled

Object Lock for Compliance

For regulatory requirements like SEC 17a-4 or HIPAA, use S3 Object Lock to enforce write-once-read-many (WORM) storage.

# Create bucket with Object Lock enabled (must be set at creation)
aws s3api create-bucket \
  --bucket compliance-bucket \
  --object-lock-enabled-for-bucket \
  --region us-east-1

# Set a default retention policy
aws s3api put-object-lock-configuration \
  --bucket compliance-bucket \
  --object-lock-configuration '{
    "ObjectLockEnabled": "Enabled",
    "Rule": {
      "DefaultRetention": {
        "Mode": "COMPLIANCE",
        "Days": 365
      }
    }
  }'

A Complete Secure Bucket Template

Here is a CloudFormation template that combines all the practices discussed.

AWSTemplateFormatVersion: '2010-09-09'
Description: Fully secured S3 bucket

Resources:
  EncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      Description: S3 bucket encryption key
      EnableKeyRotation: true
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: EnableRootAccess
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'kms:*'
            Resource: '*'

  SecureBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
              KMSMasterKeyID: !Ref EncryptionKey
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: Enabled
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      LoggingConfiguration:
        DestinationBucketName: !Ref LogBucket
        LogFilePrefix: access-logs/

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref SecureBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: DenyInsecureTransport
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource:
              - !GetAtt SecureBucket.Arn
              - !Sub '${SecureBucket.Arn}/*'
            Condition:
              Bool:
                aws:SecureTransport: 'false'

Wrapping Up

S3 security is not a single setting — it is a layered approach. Start with account-level public access blocks, enforce HTTPS and encryption through bucket policies, use SSE-KMS for sensitive data with key rotation enabled, and monitor access with server access logging and CloudTrail. Disable ACLs in favor of IAM policies and bucket policies for clearer, auditable access control. With these layers in place, your S3 buckets move from potential liability to a hardened data store.