Mastodon

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 $).

  1. 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.

  2. 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.

  3. 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.

  4. 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:

  1. We consider SCP as too complex - they require using AWS Organizations.
  2. 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.

  1. 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
    
  2. 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();
    
  3. We can test out code with cdk synth, to make sure it works.

  4. 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});
    
  5. 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.

  1. Let's just comment on one property in src/main.ts:

    1// `blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,`
    

    And see what will happen.

  2. 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.