Blog

Secure S3 Bucket constructs with CDK version 2

24 Jan, 2022
Xebia Background Header Wave

Background

As mentioned in my previous blog, enterprises often have strict security standards in place. Such as requirements that deployments must occur via Infrastructure as Code, deployed to AWS accounts via pipelines etc. Most enterprises take it up a notch with describing how resources should be configured securely in the cloud. Services such as AWS Organizations, SecurityHub, Config, Inspector and GuardDuty make it possible to control and evaluate checks and measure these results with the Companies Security Framework. This blog will describe how to make a secure CDK S3 Bucket construct to comply with the Security Framework of such an enterprise.

Prerequisites

  • Access to an AWS Account with proper rights to deploy resources.

  • CDK knowledge. As I will start with directly talking about CDK and CDK constructs, knowledge on these topics are a prerequisite. Luckily AWS created workshops on this topic. Please check them out if you want to try out CDK and CDK pipelines.

  • Projen, a new generation of project generators, which is very handy to create your own CDK construct and let it be published to artifact repositories like pypi or npm.

I’ve chosen to use Projen. It comes out of the box with the ability to create AWS CDK Typescript Constructs and set everything up for you on the integration part with Github.

Installation

The idea here is to create a S3 bucket construct to comply with the Security Framework. This will make chief information security officers (CISO) happy within an enterprise.

So first let’s start with creating a directory and install projen:

➜  mkdir secure_bucket_construct && cd secure_bucket_construct
➜  npm install -g projen
➜  npx projen new awscdk-construct

This last commando will create a projen project to build our S3 secure bucket construct in.

Real World Scenario

At the company where I’m located now, a strict Security Framework has been implemented. This strict framework results in AWS Config rules which check your resources and mark them as Noncompliant when they don’t match that particular rule. As a DevOps team we need to make all our resources compliant before we get a sign off to move with our application to User Acceptance Testing (UAT) account. As described in my previous blog, the whole application is deployed via AWS CDK.

One of those resources which pop up as Noncompliant resources when deployed out of the box via CDK, are S3 Buckets. The reason for not being compliant to the Security Framework is that an S3 Bucket needs to have the following configurations in place:

  • Public access needs to be blocked

  • Versioning needs to be enabled

  • Connection to a bucket must use Secure Socket Layer

  • Access logging needs to be enabled on the bucket

  • Bucket and content needs to be encrypted by a customer managed KMS key

Our team at the enterprise I’m working for uses a lot of buckets within their application. With normal implementation of the Level 2 S3 construct as created by the CDK team, the code would look like this:

new Bucket(this, 'Bucket',{
  enforceSSL: true,
  versioned: true,
  accessControl: BucketAccessControl.LOG_DELIVERY_WRITE,
  serverAccessLogsPrefix: 'access-logs',
  blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
  encryption: BucketEncryption.KMS,
  encryptionKey: new Key(this, 'BucketKey', {
    enableKeyRotation: true,
    trustAccountIdentities: true,
  })
})

This ends up in 12 rows of code. Imagine creating a lot of buckets in your application. Of course you can create a global function for that but what if you want to share it within your organisation?

Wouldn’t it be more convenient to just import a secure S3 Bucket construct which takes care of all this, from a Security and DevOps point of view? I think it is, so let’s actually start with our Secure S3 Bucket construct.

Go Build

Ok, let’s start building. Start with opening the .projenrc.js file in your favourite code editor. Mine looks like the following.

const    = require('projen');
const project = new awscdk.AwsCdkConstructLibrary({
  author: 'Yvo van Zee',
  authorAddress: 'yvo@yvovanzee.nl',
  cdkVersion: '2.4.0'
  defaultReleaseBranch: 'main',
  name: 'secure_bucket_construct',
  repositoryUrl: 'https://github.com/yvthepief/secure_bucket_construct.git',

  // deps: [],                /* Runtime dependencies of this module. */
  // description: undefined,  /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],             /* Build dependencies for this module. */
  // packageName: undefined,  /* The "name" in package.json. */
  // release: undefined,      /* Add release management to this project. */
});
project.synth();

Thing to notice here is that I’m using the new CDK version 2. Next thing is to install the CDK dependencies. As CDK version 2 only has two ‘base’ packages which are needed, it makes it a lot easier with dependency hell. So let’s add the aws-cdk-lib and constructs packages. Add it to the .projenrc.js file:

  peerDependencies: [
    'aws-cdk-lib',
    'constructs'
  ],

When you add or change the configuration of .projenrc.js you need to run the npx projen command. This will install all packages and generate configuration files managed by projen.

src code

Because we have our guidelines for the secure bucket ready, we can directly start with building the construct. Open the file <strong>src/index.ts</strong>. And replace the hello world example with the following code:

// Import necessary packages
import    from 'aws-cdk-lib/aws-kms';
import { BlockPublicAccess, Bucket, BucketAccessControl, BucketEncryption, BucketProps } from 'aws-cdk-lib/aws-s3';
import    from 'constructs';

export class SecureBucket extends Construct {
  public bucket: Bucket;

  // Optional allow pass of bucket props
  constructor(scope: Construct, id: string, props?: BucketProps) {
    super(scope, id);

    // Overrule Bucket Props with secure defaults and make them mandatory
    let newProps: BucketProps = {
      ...props,
      // Force encryption to use a Custom Managed Key
      encryption: props && props.encryption && props.encryption != BucketEncryption.UNENCRYPTED
        ? props.encryption
        : BucketEncryption.KMS,
      // Create the Encryption Key, with Rotation enabled
      encryptionKey: new Key(this, $61dc0f4a5c5b192856ecb6ea-key, { enableKeyRotation: true }),
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      versioned: true,
      enforceSSL: true,
      accessControl: BucketAccessControl.LOG_DELIVERY_WRITE,
      serverAccessLogsPrefix: 'access-logs',
    };

    // Create actual bucket
    this.bucket = new Bucket(this, $61dc0f4a5c5b192856ecb6ea-bucket, newProps);
  }
}

Let’s break the code down.

The imports here are the new style imports for CDK version 2. All packages are bundled under the <strong>aws-cdk-lib</strong> package. For a secure bucket we need the <strong>aws-kms</strong> and the <strong>aws-s3</strong> modules.

The export is of type Construct instead of Stack, as we are creating a Construct here. Further, the bucket props are passed and overwritten with the secure defaults. For example, enable encryption but only allow KMS, which is a Custom Managed Key (CMK), and create the key on the fly.

Other options match the security rules mentioned earlier. Lastly is to create the actual bucket with the new properties.

Testing

As the bucket construct looks fine this way, the only way to know for sure is to add testing to the project as well. Rename the <strong>test/hello.test.ts</strong> file to a more appropriate name, as long as it ends with <strong>test.ts</strong>.

For testing the aws-cdk-lib has an assertions module available. With assertions it is possible to write tests against CDK applications, with focus on CloudFormation templates.

Again, let’s start with the imports.

import { App, Stack } from 'aws-cdk-lib';
import { Match, Template } from 'aws-cdk-lib/assertions';
import { Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3';
import    from '../src';

test('Exposes underlying bucket', () => {
  const mockApp = new App();
  const stack = new Stack(mockApp, 'testing-stack');

  const bucketWrapper = new SecureBucket(stack, 'testing', {});
  expect(bucketWrapper.bucket).toBeInstanceOf(Bucket);
});

Above, a first test is also created. What this test does is create a mock App with a test Stack. A secure bucket is created and added to the test stack and stored as bucketWrapper. Then the actual test checks if the created bucket with our construct <strong>bucketWrapper.bucket</strong> is an instance Bucket from the S3 module.

Now it is time to test if our first test is correct. Run <strong>npx projen test</strong> on the command line. This test fails because our imports. We have imports we do not use with only this single test. The assertions and BucketEncryption aren’t used yet, but will be used later on. Projen will fail the test.

One of the advantages of projen is to keep your project lean and mean. After disabling some imports the test runs fine.

➜  secure_bucket_construct git:(main) ✗ npx projen test
yarn run v1.22.17
$ npx projen test
  test | jest --passWithNoTests --all --updateSnapshot
 PASS  test/secure_bucket.test.ts (7.999 s)
  ✓ Exposes underlying bucket (11 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |       60 |     100 |     100 |                   
 index.ts |     100 |       60 |     100 |     100 | 17                
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.156 s
Ran all test suites.
  test » eslint | eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js
✨  Done in 21.29s.

Now more tests can be added. Tests like:

  • Has one encrypted Bucket

  • Has BucketVersioning enabled

  • Does not allow to have BucketVersioning disabled

  • Has BlockPublicAccess to BLOCK_ALL

  • Has Bucket Logging enabled

  • Does not allow for unencrypted buckets

  • Does not allow for unencrypted uploads

  • Uses KMS encryption with key rotation on key

To not make this article extra long with all the tests explained, you can find the code for the extra test in the GitHub project.

Distribute

With projen it’s possible to distribute your builded package to NPM and other package providers. All you have to do is set up a GitHub secret for your repository and generate for example a NPM token. For a good example, please check projen-test documentation.

Try it yourself

Experiment with projen to build your own constructs. The code can be found on my GitHub.

Yvo van Zee
I'm an AWS Cloud Consultant working at Oblivion. I specialised myself in architecting and building high available environments using automation (Infrastructure as Code combined with Continuous Integration and Continuous Delivery (CI/CD)). Challenging problems and finding solutions which fit are my speciality.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts