How to secure your S3 bucket by default?

In September this year, I will officially mark 10 years in the IT
box.
For most of my professional life, I was focused on system administration,
automation, DevOps, and a bit of public/hybrid cloud.
There was always security, but rather a nice-to-have topic, not
the main pillar. Some time ago I decided, that I would try to focus
more on privacy and information security, especially now in an era of AI,
Vibe Coding, and new hyper-personalized
ads idea.
What does it mean for you, my friends? You will find more
content about securing your workload, networks, and app here. Also from
time to time, I will post some notes from my self-hosting journey
and attempts to avoid big-tech solutions. After this long intro,
let's focus on today's topic - S3 security.
Introduction
Five days ago I saw an article about yet another data leak.
Tl;dr;
The app, is designed to track productivity by Logging activity and snapping regular screenshots of employees’ screens, left over 21 million images exposed in an unsecured Amazon S3 bucket, broadcasting how workers go about their day frame by frame.
Besides the fact of an unexpected level of workplace surveillance and lack of respect for your employees. We will focus on something a bit more simple to fix - an unsecured Amazon S3 bucket.
S3 buckets
At this point let's get some scale statistics. Based on official AWS News Blog
I’m always astonished to learn about the scale at which we operate Amazon S3: It currently holds over 400 trillion objects, and exabytes of data, and processes a mind-blowing 150 million requests per second. Just a decade ago, not even 100 customers were storing more than a petabyte (PB) of data on S3. Today, thousands of customers have surpassed the 1 PB milestone.
It's hard to find official data about the scale, number of S3s globally, or number of new buckets created per hour. The only thing we can be sure of is that's massive. Let's then make the simple assumption that users create one bucket per minute is way too low, however, it's still a lot of endpoints that need to be secure.
Amazon does care about users' data security, so we can use multiple services for keeping our data secure (mostly for $).
Amazon Macie
Service which is a data scanner, that is used mostly for scanning the content of your objects in advance of looking for sensitive data, where you can read about it here if you're interested. Also, it can be setting public-access enablement.
IAM Access Analyzer for S3
Another service that can be used for detecting AWS S3 configuration. The important part here is that it's free of charge.
AWS Config
Service used for checking resource compliance with the rules. When a non-compliant resource will be fine, the service will mark it as that, and allow informing the user about this situation. Here we can configure rules about the public avalible of S3 endpoints.
Amazon GuardDuty
A threat detection service which is used for log monitoring. That can identify suspicious and potentially malicious activity. There is even a specific plan for S3 called S3 Protection
On top of that, we have Service Control Policies (SCPs) on AWS Organizations, that can block public access by default. And not-so-new annoucment (Dec, 2022), that Amazon S3 will automatically enable S3 Block Public Access and disable access control lists for all new buckets starting in April 2023.
So as we can see, we have at least 6 easy to implement mechanisms, that can check, scan, monitor, or even block by default public access.
Why does it still happen?
Then at this point, you can ask how it's possible, that people are missing this critical setting? Even if:
- We consider SCP as too complex - they require using AWS Organizations.
- Macie, Config, and GuardDuty are too expensive for us.
We still have the IAM Access Analyzer for S3, right? Yes, but no. Based on documentation:
You must visit the IAM console and enable IAM Access Analyzer on a per-Region basis.
So here we need to have at least some knowledge about this tool.
What could be other challenges? For example flexible access control system. A bucket can have read or write access provided through:
- a bucket access control list (ACL)
- a bucket policy
- a Multi-Region Access Point policy
- or an access point policy.
On top of it, our developers/businesses/whatever can:
Requires public access to support their specific use case
What we can do then?
First point, unless you share the data, you would like to share with anyone on the Internet. For example, an open-source pre-builded package, or a great collection of generally acceptable memes. Keep it private. Try to talk with the business, and explain that 21 million employees' screenshots are not good material for public buckets, as well as commercials of your solution.
Now when our businesses understand, that we're not so special. Let's try to implement some methods while coding, and for free.
AWS CDK
First I will try to show AWS CDK-based solution which will contain scanning with checkov as well as unit tests.
Init repository
As you may remember I'm using the project for my CDK apps, so I just need:
1> npx projen new awscdk-app-ts 2 3[...] 4Initialized empty Git repository in /Users/kuba/Code/AWS-samples/security-checks/.git/ 5[main (root-commit) d864dcd] chore: project created with projen 6 22 files changed, 5959 insertions(+) 7 create mode 100644 .eslintrc.json 8 create mode 100644 .gitattributes 9 create mode 100644 .github/pull_request_template.md 10 create mode 100644 .github/workflows/build.yml 11 create mode 100644 .github/workflows/pull-request-lint.yml 12 create mode 100644 .github/workflows/upgrade.yml 13 create mode 100644 .gitignore 14 create mode 100644 .mergify.yml 15 create mode 100644 .npmignore 16 create mode 100644 .projen/deps.json 17 create mode 100644 .projen/files.json 18 create mode 100644 .projen/tasks.json 19 create mode 100644 .projenrc.ts 20 create mode 100644 LICENSE 21 create mode 100644 README.md 22 create mode 100644 cdk.json 23 create mode 100644 package.json 24 create mode 100644 src/main.ts 25 create mode 100644 test/main.test.ts 26 create mode 100644 tsconfig.dev.json 27 create mode 100644 tsconfig.json 28 create mode 100644 yarn.lock
Let's tweak our
src/main.ts
1import { App, Stack, StackProps } from 'aws-cdk-lib'; 2import { Construct } from 'constructs'; 3import * as s3 from 'aws-cdk-lib/aws-s3'; 4import { RemovalPolicy } from 'aws-cdk-lib'; 5 6export class MyStack extends Stack { 7 constructor(scope: Construct, id: string, props: StackProps = {}) { 8 super(scope, id, props); 9 10 new s3.Bucket(this, 'Bucket', { 11 blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 12 encryption: s3.BucketEncryption.S3_MANAGED, 13 versioned: true, 14 removalPolicy: RemovalPolicy.DESTROY, 15 }); 16 } 17} 18 19const devEnv = { 20 account: process.env.CDK_DEFAULT_ACCOUNT, 21 region: process.env.CDK_DEFAULT_REGION, 22}; 23 24const app = new App(); 25 26new MyStack(app, 'security-checks-dev', { env: devEnv }); 27 28app.synth();
We can test out code with
cdk synth
, to make sure it works.Let's implement some basic tests then into
test/main.test.ts
1import { App } from 'aws-cdk-lib'; 2import { Template } from 'aws-cdk-lib/assertions'; 3import { MyStack } from '../src/main'; 4 5describe('S3 Bucket Properties', () => { 6 let template: Template; 7 8 beforeAll(() => { 9 const app = new App(); 10 const stack = new MyStack(app, 'test-bucket-properties'); 11 template = Template.fromStack(stack); 12 }); 13 14 it('should have Block Public Access enabled', () => { 15 template.hasResourceProperties('AWS::S3::Bucket', { 16 PublicAccessBlockConfiguration: { 17 BlockPublicAcls: true, 18 BlockPublicPolicy: true, 19 IgnorePublicAcls: true, 20 RestrictPublicBuckets: true, 21 }, 22 }); 23 }); 24 25 it('should have S3 Managed Encryption enabled', () => { 26 template.hasResourceProperties('AWS::S3::Bucket', { 27 BucketEncryption: { 28 ServerSideEncryptionConfiguration: [ 29 { 30 ServerSideEncryptionByDefault: { 31 SSEAlgorithm: 'AES256', 32 }, 33 }, 34 ], 35 }, 36 }); 37 }); 38 39 it('should have Versioning enabled', () => { 40 template.hasResourceProperties('AWS::S3::Bucket', { 41 VersioningConfiguration: { 42 Status: 'Enabled', 43 }, 44 }); 45 }); 46});
Then just run tests with Projen
1> projen test 2👾 test | jest --passWithNoTests --updateSnapshot 3 PASS test/main.test.ts (25.414 s) 4 S3 Bucket Properties 5 ✓ should have Block Public Access enabled (4 ms) 6 ✓ should have S3 Managed Encryption enabled (1 ms) 7 ✓ should have Versioning enabled 8 9----------|---------|----------|---------|---------|------------------- 10File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 11----------|---------|----------|---------|---------|------------------- 12All files | 100 | 100 | 100 | 100 | 13 main.ts | 100 | 100 | 100 | 100 | 14----------|---------|----------|---------|---------|------------------- 15Test Suites: 1 passed, 1 total 16Tests: 3 passed, 3 total 17Snapshots: 0 total 18Time: 25.612 s, estimated 28 s 19Ran all test suites.
The issue here is that we need to implement those tests on our own. Then let's try Checkov, just please remember, that raw code is not supported, we need to generate a CFn template, and test against it.
Let's just comment on one property in
src/main.ts
:1// `blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,`
And see what will happen.
Run synth and Chekhov.
1❯ cdk synth && checkov -f cdk.out/* 2[ cloudformation framework ]: 100%|████████████████████|[1/1], Current File Scanned=/cdk.out/sec 3[ kubernetes framework ]: 100%|████████████████████|[4/4], Current File Scanned=cdk.out/tree.jso 4[ secrets framework ]: 100%|████████████████████|[5/5], Current File Scanned=cdk.out/tree.json 5[ secrets framework ]: 80%|████████████████ |[4/5], Current File Scanned=cdk.out/tree.json 6 _ _ 7 ___| |__ ___ ___| | _______ __ 8 / __| '_ \ / _ \/ __| |/ / _ \ \ / / 9 | (__| | | | __/ (__| < (_) \ V / 10 \___|_| |_|\___|\___|_|\_\___/ \_/ 11 12By Prisma Cloud | version: 3.2.410 13cloudformation scan results: 14Passed checks: 4, Failed checks: 5, Skipped checks: 0 15 16Check: CKV_AWS_19: "Ensure the S3 bucket has server-side-encryption enabled" 17 PASSED for resource: AWS::S3::Bucket.Bucket83908E77 18 File: /cdk.out/security-checks-dev.template.json:3-24 19 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-14-data-encrypted-at-rest 20Check: CKV_AWS_20: "Ensure the S3 bucket does not allow READ permissions to everyone" 21 PASSED for resource: AWS::S3::Bucket.Bucket83908E77 22 File: /cdk.out/security-checks-dev.template.json:3-24 23 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-1-acl-read-permissions-everyone 24Check: CKV_AWS_57: "Ensure the S3 bucket does not allow WRITE permissions to everyone" 25 PASSED for resource: AWS::S3::Bucket.Bucket83908E77 26 File: /cdk.out/security-checks-dev.template.json:3-24 27 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-2-acl-write-permissions-everyone 28Check: CKV_AWS_21: "Ensure the S3 bucket has versioning enabled" 29 PASSED for resource: AWS::S3::Bucket.Bucket83908E77 30 File: /cdk.out/security-checks-dev.template.json:3-24 31 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-16-enable-versioning 32Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled" 33 FAILED for resource: AWS::S3::Bucket.Bucket83908E77 34 File: /cdk.out/security-checks-dev.template.json:3-24 35 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/s3-13-enable-logging 36 37 3 | "Bucket83908E77": { 38 4 | "Type": "AWS::S3::Bucket", 39 5 | "Properties": { 40 6 | "BucketEncryption": { 41 7 | "ServerSideEncryptionConfiguration": [ 42 8 | { 43 9 | "ServerSideEncryptionByDefault": { 44 10 | "SSEAlgorithm": "AES256" 45 11 | } 46 12 | } 47 13 | ] 48 14 | }, 49 15 | "VersioningConfiguration": { 50 16 | "Status": "Enabled" 51 17 | } 52 18 | }, 53 19 | "UpdateReplacePolicy": "Delete", 54 20 | "DeletionPolicy": "Delete", 55 21 | "Metadata": { 56 22 | "aws:cdk:path": "security-checks-dev/Bucket/Resource" 57 23 | } 58 24 | }, 59 60Check: CKV_AWS_53: "Ensure S3 bucket has block public ACLs enabled" 61 FAILED for resource: AWS::S3::Bucket.Bucket83908E77 62 File: /cdk.out/security-checks-dev.template.json:3-24 63 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-19 64 65 3 | "Bucket83908E77": { 66 4 | "Type": "AWS::S3::Bucket", 67 5 | "Properties": { 68 6 | "BucketEncryption": { 69 7 | "ServerSideEncryptionConfiguration": [ 70 8 | { 71 9 | "ServerSideEncryptionByDefault": { 72 10 | "SSEAlgorithm": "AES256" 73 11 | } 74 12 | } 75 13 | ] 76 14 | }, 77 15 | "VersioningConfiguration": { 78 16 | "Status": "Enabled" 79 17 | } 80 18 | }, 81 19 | "UpdateReplacePolicy": "Delete", 82 20 | "DeletionPolicy": "Delete", 83 21 | "Metadata": { 84 22 | "aws:cdk:path": "security-checks-dev/Bucket/Resource" 85 23 | } 86 24 | }, 87 88Check: CKV_AWS_54: "Ensure S3 bucket has block public policy enabled" 89 FAILED for resource: AWS::S3::Bucket.Bucket83908E77 90 File: /cdk.out/security-checks-dev.template.json:3-24 91 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-20 92 93 3 | "Bucket83908E77": { 94 4 | "Type": "AWS::S3::Bucket", 95 5 | "Properties": { 96 6 | "BucketEncryption": { 97 7 | "ServerSideEncryptionConfiguration": [ 98 8 | { 99 9 | "ServerSideEncryptionByDefault": { 100 10 | "SSEAlgorithm": "AES256" 101 11 | } 102 12 | } 103 13 | ] 104 14 | }, 105 15 | "VersioningConfiguration": { 106 16 | "Status": "Enabled" 107 17 | } 108 18 | }, 109 19 | "UpdateReplacePolicy": "Delete", 110 20 | "DeletionPolicy": "Delete", 111 21 | "Metadata": { 112 22 | "aws:cdk:path": "security-checks-dev/Bucket/Resource" 113 23 | } 114 24 | }, 115 116Check: CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled" 117 FAILED for resource: AWS::S3::Bucket.Bucket83908E77 118 File: /cdk.out/security-checks-dev.template.json:3-24 119 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-21 120 121 3 | "Bucket83908E77": { 122 4 | "Type": "AWS::S3::Bucket", 123 5 | "Properties": { 124 6 | "BucketEncryption": { 125 7 | "ServerSideEncryptionConfiguration": [ 126 8 | { 127 9 | "ServerSideEncryptionByDefault": { 128 10 | "SSEAlgorithm": "AES256" 129 11 | } 130 12 | } 131 13 | ] 132 14 | }, 133 15 | "VersioningConfiguration": { 134 16 | "Status": "Enabled" 135 17 | } 136 18 | }, 137 19 | "UpdateReplacePolicy": "Delete", 138 20 | "DeletionPolicy": "Delete", 139 21 | "Metadata": { 140 22 | "aws:cdk:path": "security-checks-dev/Bucket/Resource" 141 23 | } 142 24 | }, 143 144Check: CKV_AWS_56: "Ensure S3 bucket has RestrictPublicBuckets enabled" 145 FAILED for resource: AWS::S3::Bucket.Bucket83908E77 146 File: /cdk.out/security-checks-dev.template.json:3-24 147 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-22 148 149 3 | "Bucket83908E77": { 150 4 | "Type": "AWS::S3::Bucket", 151 5 | "Properties": { 152 6 | "BucketEncryption": { 153 7 | "ServerSideEncryptionConfiguration": [ 154 8 | { 155 9 | "ServerSideEncryptionByDefault": { 156 10 | "SSEAlgorithm": "AES256" 157 11 | } 158 12 | } 159 13 | ] 160 14 | }, 161 15 | "VersioningConfiguration": { 162 16 | "Status": "Enabled" 163 17 | } 164 18 | }, 165 19 | "UpdateReplacePolicy": "Delete", 166 20 | "DeletionPolicy": "Delete", 167 21 | "Metadata": { 168 22 | "aws:cdk:path": "security-checks-dev/Bucket/Resource" 169 23 | } 170 24 | },
As a run summary, use unit tests for business logic-related code. If you are building modules for many users, use the unit test. For security-oriented scans use check, it will be easier, and faster, and the output will be more verbose. Also, it's easy to combine with the CI/CD pipeline and can be used with multiple solutions.
Terraform
Now just jump into terraform code, with a slightly different approach.
Our initial code will be:
1terraform {
2 required_providers {
3 aws = {
4 source = "hashicorp/aws"
5 version = "~> 5.96"
6 }
7 }
8
9 required_version = ">= 1.2.0"
10}
11
12provider "aws" {
13 region = "eu-central-2"
14}
15
16resource "aws_s3_bucket" "example" {
17 bucket = "my-example-bucket-name"
18
19 tags = {
20 Environment = "Production"
21 }
22}
Then fast s3 scan and:
1> checkov -d .
2[ terraform framework ]: 100%|████████████████████|[1/1], Current File Scanned=main.tf
3[ secrets framework ]: 100%|████████████████████|[1/1], Current File Scanned=./main.tf
4
5
6 _ _
7 ___| |__ ___ ___| | _______ __
8 / __| '_ \ / _ \/ __| |/ / _ \ \ / /
9 | (__| | | | __/ (__| < (_) \ V /
10 \___|_| |_|\___|\___|_|\_\___/ \_/
11
12By Prisma Cloud | version: 3.2.410
13
14terraform scan results:
15Passed checks: 2, Failed checks: 0, Skipped checks: 0
16
17Check: CKV_AWS_93: "Ensure S3 bucket policy does not lockout all but root user. (Prevent lockouts needing root account fixes)"
18 PASSED for resource: aws_s3_bucket.example
19 File: /main.tf:16-22
20 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-24
21Check: CKV_AWS_41: "Ensure no hard coded AWS access key and secret key exists in provider"
22 PASSED for resource: aws.default
23 File: /main.tf:12-14
24 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/secrets-policies/bc-aws-secrets-5
This is good, right? No issues. Nope. It means that our bucket does not have any policy, so there is nothing to check. We need to add a bit more code:
1[...]
2resource "aws_s3_bucket_versioning" "example" {
3 bucket = aws_s3_bucket.example_bucket.id
4 versioning_configuration {
5 status = "Enabled"
6 }
7}
8
9resource "aws_s3_bucket_public_access_block" "example" {
10 bucket = aws_s3_bucket.example.id
11
12 block_public_acls = true
13 block_public_policy = true
14 ignore_public_acls = true
15 restrict_public_buckets = true
16}
17
18resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
19 bucket = aws_s3_bucket.example.id
20
21 rule {
22 apply_server_side_encryption_by_default {
23 sse_algorithm = "AES256"
24 }
25 }
26}
The results are much more informative:
1❯ checkov -d .
2[ terraform framework ]: 100%|████████████████████|[1/1], Current File Scanned=main.tf
3[ secrets framework ]: 100%|████████████████████|[1/1], Current File Scanned=./main.tf
4[ secrets framework ]: 0%| |[0/1], Current File Scanned=./main.tf
5
6 _ _
7 ___| |__ ___ ___| | _______ __
8 / __| '_ \ / _ \/ __| |/ / _ \ \ / /
9 | (__| | | | __/ (__| < (_) \ V /
10 \___|_| |_|\___|\___|_|\_\___/ \_/
11
12By Prisma Cloud | version: 3.2.410
13
14terraform scan results:
15Passed checks: 6, Failed checks: 0, Skipped checks: 0
16
17Check: CKV_AWS_41: "Ensure no hard coded AWS access key and secret key exists in provider"
18 PASSED for resource: aws.default
19 File: /main.tf:12-14
20 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/secrets-policies/bc-aws-secrets-5
21Check: CKV_AWS_93: "Ensure S3 bucket policy does not lockout all but root user. (Prevent lockouts needing root account fixes)"
22 PASSED for resource: aws_s3_bucket.example
23 File: /main.tf:16-22
24 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-24
25Check: CKV_AWS_53: "Ensure S3 bucket has block public ACLS enabled"
26 PASSED for resource: aws_s3_bucket_public_access_block.example_public_access_block
27 File: /main.tf:31-38
28 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-19
29Check: CKV_AWS_54: "Ensure S3 bucket has block public policy enabled"
30 PASSED for resource: aws_s3_bucket_public_access_block.example_public_access_block
31 File: /main.tf:31-38
32 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-20
33Check: CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled"
34 PASSED for resource: aws_s3_bucket_public_access_block.example_public_access_block
35 File: /main.tf:31-38
36 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-21
37Check: CKV_AWS_56: "Ensure S3 bucket has 'restrict_public_buckets' enabled"
38 PASSED for resource: aws_s3_bucket_public_access_block.example_public_access_block
39 File: /main.tf:31-38
40 Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-22
Summary
As you have seen, keeping S3 secure looks like a simple topic. However it's easy to mess up with configuration, go too far with a special use-case, or just overwrite our config with one of the many options.
It could be improved by keeping configuration as code in sync with our infra. Probably that's the best way, just after the secure design of the initial solution to keeping your system in shape.
How do you like a more verbose type of blog? Is it fine? Let me know, and ping me on Mastodon.
Where starting from today code used, can be found at codeberg.