S3 Security Best Practices

Ruchir B Pandya
7 min readNov 4, 2023

--

Fun Fact: Access keys are long-term credentials for an IAM user or the AWS account root user. You can use access keys to sign programmatic requests to the AWS CLI or AWS API (directly or using the AWS SDK). Access keys consist of two parts: an access key ID (for example, AKIAIOSFODNN7EXAMPLE) and a secret access key (for example, wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY). Like a user name and password, you must use both the access key ID and secret access key together to authenticate your requests. Manage your access keys as securely as you do your user name and password.

The following exercises will show you how to use bucket policies, access points, and Block Public Access to secure your S3 buckets.

In this exercise we will create a S3 Bucket Policy that requires connections to use HTTPS.

From the AWS console, click Services and select S3.

Click on the Permissions tab.

Under Bucket Policy click Edit.

Copy the bucket policy below, and paste into the Bucket Policy Editor.

{
"Statement": [
{
"Action": "s3:*",
"Effect": "Deny",
"Principal": "*",
"Resource": "arn:aws:s3:::BUCKET_NAME/*",
"Condition": {
"Bool": {
"aws:SecureTransport": false
}
}
}
]
}

Replace BUCKET_NAME with the bucket name you copied to your text editor. Make sure you do not delete the /* a the end of the bucket name. It should look similar to the policy below.

Click Save changes.

Open an SSH session to the SID-security-instance using EC2 Instance Connect if it is not already open. Run the following command.

aws s3api head-object --key app1/file1 --endpoint-url http://s3.amazonaws.com --profile user1 --bucket ${bucket}

The command should return a 403 error since the endpoint-url is HTTP.

Now run the following command in your SSH session.

aws s3api --endpoint-url https://s3.amazonaws.com --profile user1 head-object --key app1/file1 --bucket ${bucket}

The command succeeded because the endpoint-url uses HTTPS as required by the bucket policy.

Block Public ACLs

In this exercise we will demonstrate the use of block public ACLs.

From the AWS console, click Services and select S3.

Click on the Permissions tab.

Under Bucket Policy click Edit.

Copy the bucket policy below and paste into the Bucket Policy Editor.

{
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::BUCKET_NAME/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "private"
}
}
}
]
}

Replace BUCKET_NAME with your bucket name and click Save changes.

Open an SSH session to the SID-security-instance using EC2 Instance Connect if it is not already open. Run the following command.

aws s3api put-object --key text01 --body textfile --profile user1 --bucket ${bucket}

The request should succeed since the default for an object ACL is private.

Now run the following command in your SSH session.

aws s3api put-object --key text01 --body textfile --acl public-read --profile user1 --bucket ${bucket}

This command also succeeded, but it is not the behavior one would expect. Why do we still have access?

The current bucket policy allows ACLs that are private but doesn’t DENY anything. It is important to write policies that prevent actions, not allow it when trying to restrict actions against a bucket. The current bucket policy also allows Public access to the bucket unintentionally due to the principal being a wildcard.

Replace the existing policy with the following policy. Replace BUCKET_NAME with your bucket name. Notice this is a deny policy.

{
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::BUCKET_NAME/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": [
"public-read",
"public-read-write",
"authenticated-read"
]
}
}
}
]
}

Click Save changes

Run the following command in the SSH session.

aws s3api put-object --key text01 --body textfile --profile user1 --bucket ${bucket}

The request should succeed since the default for an object ACL is private.

Now run the following command in the SSH session.

aws s3api put-object --key text01 --body textfile --acl public-read --profile user1 --bucket ${bucket}

The request fails as the bucket policy now restricts public-read ACL.

Configure S3 Block Public Access

In this exercise we will configure S3 Block Public Access, an easy way to prevent public access to your bucket.

From the AWS console, click Services and select S3.

Under Block public access (bucket settings) click Edit.

Select Block public access to buckets and objects granted through new access control lists (ACLs).

Click Save changes.

Type confirm to confirm the new settings then click Confirm.

From your SSH session run the following command.

aws s3api put-object --key text01 --body textfile --profile user1 --bucket ${bucket}

The request succeeds since the deafult for an object ACL is private.

From you SSH session run the following command.

aws s3api put-object --key text01 --body textfile --acl public-read --profile user1 --bucket ${bucket}

The requests fails as the bucket policy restricts the public-read ACL.

Restrict Access to a S3 VPC Endpoint

You can simplify access to S3 resources from within a VPC by using a VPC Endpoint. These endpoints are easy to configure, highly reliable, and provide a private connection to S3 that does not require an Internet Gateway or NAT instance.

In this exercise we will configure a S3 VPC Endpoint and a bucket policy to limit access to only requests that pass through the VPC Endpoint. This is an easy way to limit access to only clients in your VPC.

From the AWS console, click Services and select VPC.

Click Endpoints on the column to the left.

Click Create Endpoint.

Enter a Name tag — sid-vpce-s3

Select AWS services as the Service category

Under Services, type S3 in the search bar and press enter. This should filter to the list of S3 endpoint services. Select the Service Name wit the Type of Gateway.

Under VPC, select the VPC that says SID-vpc. At this point your screen should look similar to the following.

Do not configure any route tables. Leave the Policy set to Full Access.

Click Create endpoint.

Copy the VPC Endpoint ID to your text editor.

From the AWS console, click Services and select S3.

Click the bucket name starting with sid-security-xxxxxxxx.

Click on the Permissions tab.

Under Bucket Policy click Edit.

Copy the bucket policy below and paste into the Bucket Policy Editor.

{
"Version": "2012-10-17",
"Statement": [
{
"Action": "s3:*",
"Effect": "Deny",
"Resource": "arn:aws:s3:::BUCKET_NAME/*",
"Condition": {
"StringNotEquals": {
"aws:sourceVpce": "VPC_ENDPOINT_ID"
}
},
"Principal": "*"
}
]

Replace BUCKET_NAME with the bucket name and VPC_ENDPOINT_ID with the Endpoint ID. You policy should look similar to the below policy.

Click Save.

From you SSH session run the following command.

aws s3api head-object --key app1/file1 --profile user1 --bucket ${bucket}

The HeadObject request failed because there is no route table associated with the VPC Endpoint

From the AWS console, click Services and select VPC.

Click Endpoints on the column to the left.

The VPC Endpoint we created early should be selected. Click Actions and select Manage Route Tables.

Select the Route Table ID associated with SID-routes.

Click Modify Route Tables.

From you SSH session run the following command.

aws s3api head-object --key app1/file1 --profile user1 --bucket ${bucket}

The request succeeds because you associated a route to the VPC Endpoint.

This was a slightly longer blog but hope this gave some insights into S3 security and various best practices to protect your S3 bucket(s)

--

--