adrianhesketh.com

Pentest passing S3 bucket CloudFormation config

As a consultant, I launch a lot of new services with customers. Before launch, there’s usually a penetration test of the system and any associated AWS infrastructure. One thing that regularly shows up on the AWS configuration side of things are S3 buckets.

Common checks include:

Unfortunately, there’s no often no explanation of why these checks are important or how to resolve them.

Access logging is useful because without it, you probably won’t have a record of if anyone accessed data. For example, let’s say that you’ve ended up with some private information in an S3 bucket. The next question will be “did anyone access the data?”. Without logs, you won’t be able to answer that question, but if you have CloudTrail logs enabled, you probably don’t need S3 logs enabled too. There’s more information on that at https://docs.aws.amazon.com/AmazonS3/latest/dev/cloudtrail-logging.html#cloudtrail-logging-s3-info

Versioning is important to ensure that maliciously overwritten data can be recovered.

Encryption at rest and in transit is usually a compliance requirement, so it makes sense to enable default encryption, and to ensure that data transfers to and from S3 use TLS. S3 is one of the few AWS services to support non-TLS access, presumably because of the Website hosting feature.

Finally, ensuring that buckets can only be deleted by a user that has used MFA recently is a useful safeguard against accidental or malicious bucket deletion.

AWS documentation at https://docs.aws.amazon.com/AmazonS3/latest/dev/security-best-practices.html also explains some of the reasoning.

To resolve some of these issues, I created a command line tool that can apply some policies to lots of buckets at once, or just a single bucket at a time. The code is available over at https://github.com/a-h/s3policy

It’s pretty easy to use, just apply the changes using the command line flags:

s3policy -bucket=name-of-bucket \
  -deleteOldVersions=true \
  -deleteAfterDays=30 \
  -logToBucket=logging-bucket-name \
  -version=true

However, if you’re using CloudFormation to configure your infrastructure, it’s rather unsatisying to do this, and new buckets that your teams create will have the same problem, so I set out to write up a good example of the CloudFormation you actually need to use to give to teams early on, rather than wait for configuration to be tested.

Log bucket

First, we need a bucket to store logs. Lets start off by creating a template with a parameter for the log bucket name:

AWSTemplateFormatVersion: "2010-09-09"
Description: Creates a bucket to store CloudTrail and S3 logs.

Parameters:
  BucketName:
    Type: String
    Description: The name of your new bucket.

Next, add our bucket.

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName

Log buckets need to have “LogDeliveryWrite” permissions applied to let AWS write logs to them:

      AccessControl: "LogDeliveryWrite"

We want our logs to be encrypted like everything else:

      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

Next, let’s make sure it’s not possible to delete the logs for at least a year. This might not be sensible for your use case, you’ll need to think about whether you want to enable this or not.

      ObjectLockEnabled: true # Prevent deletion of logs.
      ObjectLockConfiguration:
        ObjectLockEnabled: "Enabled"
        Rule:
          DefaultRetention:
            Mode: "GOVERNANCE"
            Days: 365 # Save logs for one year.

We also need to make sure we can’t accidentally make it public.

      PublicAccessBlockConfiguration: # Disallow public buckets.
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

And enable versioning:

      VersioningConfiguration: # Enable versioning.
        Status: Enabled

Now we can add some bucket policies, to disable non HTTPS access, enforce MFA for bucket deletion, and enable CloudTrail and AWS Config delivery.

  MyS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref MyS3Bucket
      PolicyDocument:
        Statement:
          - Sid: RequireTLS
            Effect: Deny
            Principal: "*"
            Action: "*"
            Resource: !Join ["", ["arn:aws:s3:::", !Ref MyS3Bucket]]
            Condition:
              Bool:
                "aws:SecureTransport": false
          - Sid: RequireMFAForBucketDeletion
            Effect: Deny
            Principal: "*"
            Action:
              - s3:DeleteBucket
            Resource: !Join ["", ["arn:aws:s3:::", !Ref MyS3Bucket]]
            Condition:
              "Null":
                "aws:MultiFactorAuthAge": true
          - Sid: EnableCloudTrailAndConfigPermissionCheck
            Effect: Allow
            Principal:
                Service:
                  - config.amazonaws.com
                  - cloudtrail.amazonaws.com
            Action:
              - s3:GetBucketAcl
            Resource: !Join ["", ["arn:aws:s3:::", !Ref MyS3Bucket]]
          - Sid: EnableCloudTrailAndConfigBucketDelivery
            Effect: Allow
            Principal:
                Service:
                  - config.amazonaws.com
                  - cloudtrail.amazonaws.com
            Action:
              - s3:PutObject
            Resource: !Join ["", ["arn:aws:s3:::", !Ref MyS3Bucket, "/*"]]

We can run this in with:

aws cloudformation deploy \
  --stack-name=log-bucket \
  --template-file=./log-bucket-template.yaml \
  --parameter-overrides BucketName=a-h-log-bucket

Data bucket

With that out of the way, we can add a data bucket. Much of the configuration is the same, but there’s no need to allow AWS to write logs to the bucket, and there’s a new LoggingConfiguration section to configure the location to write logs to.

AWSTemplateFormatVersion: "2010-09-09"
Description: Creates a bucket that passes common AWS config rules.

Parameters:
  BucketName:
    Type: String
    Description: The name of your new bucket.
  LogBucket:
    Type: String
    Description: The ARN of the S3 bucket used to store access logs.

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration: # Optional, can be used to reduce wasted storage if you don't need old versions to be retained.
        Rules:
          - AbortIncompleteMultipartUpload: # Abandon incomplete uploads after 7 days.
              DaysAfterInitiation: 7
            Status: Enabled
          - NoncurrentVersionExpirationInDays: 7 # Delete old versions of files after 7 days. Think about whether this meets your audit requirements.
            Status: Enabled
      LoggingConfiguration: # Enable access logging. The LogBucket needs to be in the same region and AWS account as this bucket.
        DestinationBucketName: !Ref LogBucket
        LogFilePrefix: !Ref BucketName # Use the name of the bucket as the prefix.
      PublicAccessBlockConfiguration: # Disallow public buckets.
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration: # Enable versioning.
        Status: Enabled
  MyS3BucketPolicy: # Policy to disable non HTTPS access, and enforce MFA delete.
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref MyS3Bucket
      PolicyDocument:
        Statement:
          - Sid: RequireTLS
            Effect: Deny
            Principal: "*"
            Action: "*"
            Resource: !Join ["", ["arn:aws:s3:::", !Ref MyS3Bucket]]
            Condition:
              Bool:
                "aws:SecureTransport": false
          - Sid: RequireMFAForBucketDeletion
            Effect: Deny
            Principal: "*"
            Action:
              - s3:DeleteBucket
            Resource: !Join ["", ["arn:aws:s3:::", !Ref MyS3Bucket]]
            Condition:
              "Null":
                "aws:MultiFactorAuthAge": true

This can be ran in with a similar command:

aws cloudformation deploy \
  --stack-name=secure-bucket \
  --template-file=./s3-bucket-template.yaml \
  --parameter-overrides BucketName=a-h-secure-bucket LogBucket=a-h-log-bucket

Summary

It takes quite a bit of additional config for an S3 bucket to pass a typical AWS configuration test from a security team. You might find the https://github.com/a-h/s3policy script or the example templates I’ve made at https://github.com/a-h/secure-bucket useful as a basis for your config.