Many applications using Amazon Web Services (AWS) will interact with the Amazon Simple Storage Service (S3) at some point, since it’s an inexpensive storage service with high availability and durability guarantees, and most native AWS services use it as a building block.

In this post I’ll go over a few of the configuration settings that you can use to secure your S3 resources, with a base CloudFormation template at the end that you can play with and extend.

My provided examples are in YAML, while you can also use JSON in CloudFormation. Which one you use is largely a matter of personal preference.

Encryption at rest

It’s a good idea to encrypt your data wherever it’s stored so that only those with access to the keys can read it. Any sensitive data should always be encrypted, and it’s usually only acceptable to leave data unencrypted if it’s intended to be readable by everyone, for all time.

AWS S3 supports several mechanisms for server-side encryption of data:

It usually makes sense to use SSE-S3 or SSE-KMS unless you have a good reason to do otherwise. SSE-S3 is very simple to use since all the details are taken care of for you, while SSE-KMS provides additional auditing and credential rotation capabilities.

The decision for which one to use usually depends on your security requirements and support by the services that will be interacting with S3. Many AWS services natively support KMS encryption, while a few services only support SSE-S3.

If both SSE-S3 and SSE-KMS are options for you, then I’d recommend using SSE-KMS with custom keys generated in KMS, since this provides you with auditing by default and allows you to disable or rotate encryption keys with minimal effort.

Enabling encryption by default

You can enable encryption by default for your S3 bucket with either SSE-S3 or SSE-KMS.

S3 bucket properties for SSE-S3 encryption:

BucketEncryption:
  ServerSideEncryptionConfiguration:
    - ServerSideEncryptionByDefault:
        SSEAlgorithm: AES256

S3 bucket properties for SSE-KMS encryption using the default account KMS key:

BucketEncryption:
  ServerSideEncryptionConfiguration:
    - ServerSideEncryptionByDefault:
        SSEAlgorithm: 'aws:kms'

S3 bucket properties for SSE-KMS encryption using a custom KMS key:

BucketEncryption:
  ServerSideEncryptionConfiguration:
    - ServerSideEncryptionByDefault:
        KMSMasterKeyID: 'Your key ID goes here'
        SSEAlgorithm: 'aws:kms'

I’d suggest setting up a custom KMS key if you want to use KMS by default since this allows you to disable and rotate your key as needed, which is a helpful security capability.

See the ServerSideEncryptionByDefault documentation for more details on these configuration options.

Requiring uploaded files to be encrypted

The following S3 bucket policy statement ensures that PutObject requests for uploading files to your S3 bucket use server-side encryption:

- Action: 's3:PutObject'
  Condition:
    'Null':
      's3:x-amz-server-side-encryption': true
  Effect: Deny
  Principal: '*'
  Resource: !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here/*'
  Sid: DenyPublishingUnencryptedResources

Sid stands for “statement identifier” and can be set to anything you like; this is primarily a label that can also be used as a sub-identifier within the policy. Sid values must be unique within a given policy, while they can be repeated across different policies.

Requiring a specific encryption mechanism to be used

The following S3 bucket policy statement requires SSE-KMS to be used if a server-side encryption header is provided:

- Action: 's3:PutObject'
  Condition:
    'StringNotEquals':
      's3:x-amz-server-side-encryption': 'aws:kms'
  Effect: Deny
  Principal: '*'
  Resource: !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here/*'
  Sid: DenyIncorrectEncryptionHeader

The following S3 bucket policy statement requires either SSE-S3 or SSE-KMS to be used if a server-side encryption header is provided:

- Action: 's3:PutObject'
  Condition:
    'ForAllValues:StringNotEquals':
      's3:x-amz-server-side-encryption':
        - AES256
        - 'aws:kms'
  Effect: Deny
  Principal: '*'
  Resource: !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here/*'
  Sid: DenyIncorrectEncryptionHeader

Encryption in transit

Any sensitive data that is being stored in S3 should be uploaded and retrieved using encrypted connections, otherwise it’s possible for the data to be read and modified between endpoints.

Requiring encrypted connections when accessing resources

The following S3 bucket policy statement requires encrypted connections when uploading or reading S3 resources:

- Action:
    - 's3:GetObject'
    - 's3:PutObject'
  Condition:
    Bool:
      'aws:SecureTransport': false
  Effect: Deny
  Principal: '*'
  Resource: !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here/*'
  Sid: DenyUnencryptedConnections

Access control

While encrypting your data at rest and in transit is important, controlling who is able to view and download sensitive files is essential, and misconfiguring S3 buckets to allow public read or write access is a security risk if you’re working with confidential data.

Using a base access control list (ACL) that grants limited permissions

You can optionally specify one of a set of predefined values for the AccessControl bucket property to use a pre-defined access control list to build on via IAM and S3 bucket policies. See the documentation for “canned” S3 ACLs for more information on the underlying permissions granted for each value.

By default, the most locked-down base ACL is used, Private, which only grants the account owner full control over the bucket and its resources by default.

BucketOwnerFullControl grants both the bucket owner and the object owner full control over an object (eg. file) that has been uploaded to the bucket, which may be helpful for some applications.

ACLs that grant public read or write access should be avoided for any buckets that store sensitive data.

Blocking public access by default

S3 bucket properties for blocking public access by default:

PublicAccessBlockConfiguration:
  BlockPublicAcls: true
  BlockPublicPolicy: true
  IgnorePublicAcls: true
  RestrictPublicBuckets: true

S3 bucket policy statements for preventing S3 requests that grant public access to resources:

- Action:
    - 's3:PutBucketAcl'
    - 's3:PutObject'
    - 's3:PutObjectAcl'
  Condition:
    StringEquals:
      's3:x-amz-acl':
        - authenticated-read
        - public-read
        - public-read-write
  Effect: Deny
  Principal: '*'
  Resource:
    - !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here'
    - !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here/*'
  Sid: DenyPublicReadAcl
- Action:
    - 's3:PutBucketAcl'
    - 's3:PutObject'
    - 's3:PutObjectAcl'
  Condition:
    StringLike:
      's3:x-amz-grant-read':
        - '*http://acs.amazonaws.com/groups/global/AllUsers*'
        - '*http://acs.amazonaws.com/groups/global/AuthenticatedUsers*'
  Effect: Deny
  Principal: '*'
  Resource:
    - !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here'
    - !Sub 'arn:${AWS::Partition}:s3:::your-bucket-name-goes-here/*'
  Sid: DenyGrantingPublicRead

Putting it together in a CloudFormation template

Below is a starter CloudFormation YAML template which applies the discussed policies to

  • enforce encryption at rest,
  • enforce encryption in transit,
  • block public access by default, and
  • block access control list changes that grant public read permissions to resources.

This example uses SSE-S3 as the default encryption algorithm and allows either SSE-S3 or SSE-KMS encryption to be used when specified, while you can use alternative values from previous sections to use SSE-KMS by default and restrict resources to using a single encryption mechanism.

Parameters:
  NameOfBucket:
    Description: 'Name of the S3 bucket, which must be globally unique'
    Type: String
Resources:
  YourS3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Ref NameOfBucket
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
  YourS3BucketPolicy:
    DependsOn:
      - YourS3Bucket
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref YourS3Bucket
      PolicyDocument:
        Statement:
          - Action: 's3:PutObject'
            Condition:
              'Null':
                's3:x-amz-server-side-encryption': true
            Effect: Deny
            Principal: '*'
            Resource: !Sub 'arn:${AWS::Partition}:s3:::${YourS3Bucket}/*'
            Sid: DenyPublishingUnencryptedResources
          - Action: 's3:PutObject'
            Condition:
              'ForAllValues:StringNotEquals':
                's3:x-amz-server-side-encryption':
                  - AES256
                  - 'aws:kms'
            Effect: Deny
            Principal: '*'
            Resource: !Sub 'arn:${AWS::Partition}:s3:::${YourS3Bucket}/*'
            Sid: DenyIncorrectEncryptionHeader
          - Action:
              - 's3:GetObject'
              - 's3:PutObject'
            Condition:
              Bool:
                'aws:SecureTransport': false
            Effect: Deny
            Principal: '*'
            Resource: !Sub 'arn:${AWS::Partition}:s3:::${YourS3Bucket}/*'
            Sid: DenyUnencryptedConnections
          - Action:
              - 's3:PutBucketAcl'
              - 's3:PutObject'
              - 's3:PutObjectAcl'
            Condition:
              StringEquals:
                's3:x-amz-acl':
                  - authenticated-read
                  - public-read
                  - public-read-write
            Effect: Deny
            Principal: '*'
            Resource:
              - !Sub 'arn:${AWS::Partition}:s3:::${YourS3Bucket}'
              - !Sub 'arn:${AWS::Partition}:s3:::${YourS3Bucket}/*'
            Sid: DenyPublicReadAcl
          - Action:
              - 's3:PutBucketAcl'
              - 's3:PutObject'
              - 's3:PutObjectAcl'
            Condition:
              StringLike:
                's3:x-amz-grant-read':
                  - '*http://acs.amazonaws.com/groups/global/AllUsers*'
                  - '*http://acs.amazonaws.com/groups/global/AuthenticatedUsers*'
            Effect: Deny
            Principal: '*'
            Resource:
              - !Sub 'arn:${AWS::Partition}:s3:::${YourS3Bucket}'
              - !Sub 'arn:${AWS::Partition}:s3:::${YourS3Bucket}/*'
            Sid: DenyGrantingPublicRead

You can add additional policy statements to whitelist specific IAM users to perform specific actions on specific resources.

Depending on your needs, you can also set up auditing and alarming for when S3 resources are accessed or when access policies are modified, update your bucket configuration to prevent data from being overwritten, and set up additional mechanisms to control where S3 queries can be initiated from.

See the S3 security best practices guide for more information on where to go next.

Resources

AWS documentation:

AWS blog posts: