Yvo van Zee
Yvo's Blog

Yvo's Blog

Secure S3 Bucket construct with CDK version 2

Secure S3 Bucket construct with CDK version 2

Yvo van Zee's photo
Yvo van Zee
·Jan 7, 2022·

8 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • Background
  • Prerequisites
  • Installation
  • Real World Scenario
  • Go Build
  • Try yourself

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 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 CDK S3 Bucket construct to comply to 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 topics. Please check them out if you want to try out on 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 a company where I'm located now, a strict Security Framework has been implemented. This strict framework results in AWS Config rules which checks 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 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, are 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,
  })
})

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 L3 Bucket construct which takes care of all this, from a Security and DevOps point of view? I think it is, so let start with our L3 Secure Bucket construct.

Go Build

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

const { awscdk } = 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 here. 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 lets 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 generates 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 src/index.ts. And replace the hello world example with the following code:

// Import necessary packages
import { Key } from 'aws-cdk-lib/aws-kms';
import { BlockPublicAccess, Bucket, BucketAccessControl, BucketEncryption, BucketProps } from 'aws-cdk-lib/aws-s3';
import { Construct } 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, `${id}-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, `${id}-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 aws-cdk-lib package. So for a secure bucket we need the aws-kms and theaws-s3 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. So rename the test/hello.test.ts file to a more appropriate name, as long as it ends with test.ts.

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

So again let 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 { SecureBucket } 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 creating 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 bucketWrapper.bucket is of instance Bucket from the S3 module.

Now it is time to test if our first test is correct. Run npx projen test 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 isn't used yet, but will be used later on. Projen will fail the test.

failed_projen_test.png

One of the advantage of projen 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 test 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 test 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 setup a github secret for your repository and generate for example a NPM token. For a good example please check projen-test documentation

Try yourself

Experiment with projen to build your own contructs. Code can be found in my GitHub

Did you find this article valuable?

Support Yvo van Zee by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this