Check and secure CDK code with cdk-nag

Check and secure CDK code with cdk-nag

Using Construct Hub cdk-nag construct to create secure CDK application code

Background

In my previous blog, CDK Pipelines and CloudFormation linting I've wrote about how you can use cfn_nag in CDK pipelines to check your CloudFormation templates on security best practises. With an end goal to make your code more secure and enterprise ready. Since 2 December last year, AWS announced the general availability of the Construct Hub.

The Construct Hub is a single home where the open-source community, AWS, and cloud technology providers can discover and share construct libraries for all CDKs.

In the Construct Hub the construct cdk-nag has been released. The cdk-nag construct allows you to check your CDK applications for best practices using a combination of available rule packs. Using this cdk-nag construct and use it inside your code to check upon security best practises, will eventually make your application more secure. In this article I will describe how to use the cdk-nag construct in combination with CDK.

Prerequisites

As this blog describes the use of CDK, knowledge of CDK is required. Luckily AWS created workshops on this topic. Please check them out if you want to try out CDK.

Furthermore access to an AWS Account with proper rights to deploy resources is needed.

Installation

The idea here is to create a basic S3 bucket, and let cdk-nag check the code by using the Aspect function in CDK. The outcome will be checked with the normal cfn_nag cli tool to see if this match. After that we will import the secure bucket construct used in my other blog to check if this will make the S3 bucket secure.

So start with creating a CDK project and work from there:

➜  mkdir s3bucket_with_cdk_nag_construct && cd s3bucket_with_cdk_nag_construct 
➜  cdk init app --language python

I'm using Python here, as the previous blogs were also using Python CDK and I find personally that there are more Typescript examples then Python

Real World Scenario

Within enterprises, security frameworks are often used to make sure that resources are securely deployed in the cloud. AWS has the service Security Hub which can deploy rulesets to support such security frameworks.

When developing a CDK application, it is sometimes hard to track if your created code is compliant or not before actually deploying your CDK stacks.

Using tools like cfn_nag will allow you to check your CloudFormation templates on security best practices. Now with the construct cdk_nag from the Construct Hub it is possible to implement cfn_nag rules and checks directly into your code set during synthesizing.

Go Build

As we are using CDK version 2, a lot of dependencies are taken away from us. We only need to add cdk-nag. In the CDK application edit the requirements.txt file and add cdk-nag. I'm using cdk-nag above version 2.10 for this example, so that I'm using the latest one.

aws-cdk-lib==2.16.0
constructs>=10.0.0,<11.0.0
cdk-nag>=2.10.0

Now install everything. First switch to the virtual environment created by the cdk init app command:

➜  cdkpipeline_with_cfn_nag_construct git:(main) ✗ source .venv/bin/activate   
(.venv) ➜  cdkpipeline_with_cfn_nag_construct git:(main) ✗ pip install -r requirements.txt
Collecting aws-cdk-lib==2.16.0
  Downloading aws_cdk_lib-2.16.0-py3-none-any.whl (64.1 MB)
     |████████████████████████████████| 64.1 MB 311 kB/s             
Collecting constructs<11.0.0,>=10.0.0
  Downloading constructs-10.0.87-py3-none-any.whl (54 kB)
     |████████████████████████████████| 54 kB 5.9 MB/s             
Collecting cdk-nag>=2.10.0
  Downloading cdk_nag-2.10.4-py3-none-any.whl (661 kB)
     |████████████████████████████████| 661 kB 8.0 MB/s            
Collecting jsii<2.0.0,>=1.54.0
  Using cached jsii-1.55.0-py3-none-any.whl (383 kB)
Collecting publication>=0.0.3
  Using cached publication-0.0.3-py2.py3-none-any.whl (7.7 kB)
Collecting typing-extensions<5.0,>=3.7
  Using cached typing_extensions-4.1.1-py3-none-any.whl (26 kB)
Collecting cattrs<1.11,>=1.8
  Using cached cattrs-1.10.0-py3-none-any.whl (29 kB)
Collecting python-dateutil
  Using cached python_dateutil-2.8.2-py2.py3-none-any.whl (247 kB)
Collecting attrs~=21.2
  Using cached attrs-21.4.0-py2.py3-none-any.whl (60 kB)
Collecting six>=1.5
  Using cached six-1.16.0-py2.py3-none-any.whl (11 kB)
Installing collected packages: six, attrs, typing-extensions, python-dateutil, cattrs, publication, jsii, constructs, aws-cdk-lib, cdk-nag
Successfully installed attrs-21.4.0 aws-cdk-lib-2.16.0 cattrs-1.10.0 cdk-nag-2.10.4 constructs-10.0.87 jsii-1.55.0 publication-0.0.3 python-dateutil-2.8.2 six-1.16.0 typing-extensions-4.1.1

Now we have installed all dependencies, it is time to add the code.

Create S3 bucket

As we are using CDK version 2 now, it is not needed to install the bucket construct as a separate pip installation. This has already been taken care of with the installation of CDK version 2 libraries.

Open the project inside your preferred editor, mine is Visual Studio Code.

Inside the the project open the file s3bucket_with_cdk_nag_construct_stack.py inside the s3bucket_with_cdk_nag_construct folder. This file will be used to create the S3 bucket.

Add to the import aws_s3 as s3 to import the S3 construct.

from aws_cdk import (
    aws_s3 as s3,
    Stack,
)
from constructs import Construct

class S3BucketWithCfnNagConstructStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3.Bucket(self, 'S3Bucket')

What is different between this stack and the one created in the CDK Pipelines and CloudFormation linting which uses CDK version 1 are the import statements.

Now synthesize the CDK project. This will show the CloudFormation template. Here is the snippet of mine:

Resources:
  S3Bucket07682993:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain

This is what a plain S3 bucket construct will produce as CloudFormation json output. If you want to run cfn_nag_scan locally, you need to install the package cfn-nag as a ruby gem:

(.venv) ➜  s3bucket_with_cdk_nag_construct git:(main) ✗ sudo gem install cfn-nag

This allows you to run cfn_nag_scan on the synthesized template in the cdk.out directory:

(.venv) ➜  s3bucket_with_cdk_nag_construct git:(main) ✗ cfn_nag_scan -i cdk.out/S3BucketWithCfnNagConstructStack.template.json
------------------------------------------------------------
cdk.out/S3BucketWithCfnNagConstructStack.template.json
------------------------------------------------------------------------------------------------------------------------
| WARN W51
|
| Resource: ["S3Bucket07682993"]
| Line Numbers: [4]
|
| S3 bucket should likely have a bucket policy
------------------------------------------------------------
| WARN W35
|
| Resource: ["S3Bucket07682993"]
| Line Numbers: [4]
|
| S3 Bucket should have access logging configured
------------------------------------------------------------
| WARN W41
|
| Resource: ["S3Bucket07682993"]
| Line Numbers: [4]
|
| S3 Bucket should have encryption option set

Failures count: 0
Warnings count: 3

As you can see the simple created bucket via the S3 bucket construct isn't that securely configured accordingly to cfn_nag. This is cool and all, but what if we can use an Aspect for cfn_nag_scan. Let start with configuring cdk_nag. Update the s3bucket_with_cdk_nag_construct_stack.py file to use the cdk_nag construct:

from aws_cdk import (
    Aspects,
    Stack,
    aws_s3 as s3,
)
from constructs import Construct
import cdk_nag

class S3BucketWithCfnNagConstructStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3.Bucket(self, 'S3Bucket')
        Aspects.of(self).add(cdk_nag.AwsSolutionsChecks())

As you can see, in the import statement, the Aspects and cdk_nag are imported. Take in account that the package name to import is with lower case instead of hyphen. The last line in the code is the actual piece which calls the cdk_nag construct. Here it is used as an Aspect on the constructs inside this stack, but you can also use cdk_nag on a stack level. Then you need to import the cdk_nag construct in the app.py file and use Aspect.of(App).add(cdk_nag.AwsSolutions(Checks()) instead. More on the options on cdk_nag can be found in the documentation on the Construct Hub

So now everything is in place, synthesize the CDK app:

(.venv) ➜  s3bucket_with_cdk_nag_construct git:(main) ✗ cdk synth
[Error at /S3BucketWithCfnNagConstructStack/S3Bucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled.

[Error at /S3BucketWithCfnNagConstructStack/S3Bucket/Resource] AwsSolutions-S2: The S3 Bucket does not have public access restricted and blocked.

[Error at /S3BucketWithCfnNagConstructStack/S3Bucket/Resource] AwsSolutions-S3: The S3 Bucket does not default encryption enabled.

[Error at /S3BucketWithCfnNagConstructStack/S3Bucket/Resource] AwsSolutions-S10: The S3 Bucket does not require requests to use SSL.


Found errors

It doesn't synthesize due to errors raised by cdk_nag. If we compare the outcome with the normal cfn_nag_scan you can see that there is an extra error raised with the cdk_nag construct.

[Error at /S3BucketWithCfnNagConstructStack/S3Bucket/Resource] AwsSolutions-S10: The S3 Bucket does not require requests to use SSL.

Make the bucket secure

In my previous post Secure S3 Bucket construct with CDK version 2 I do write about how to create a secure bucket construct. Let us try that one and see how secure it really is.

Import the secure bucket construct from pypi.org. Add the package secure-bucket-construct==2.1.0 to the requirements.txt file and run pip install -r requirements.txt again.

Now add it to the s3bucket_with_cdk_nag_construct_stack.py file so we can use it instead of the normal S3 bucket import. The s3bucket_with_cdk_nag_construct_stack.py should look like this:

from aws_cdk import (
    Aspects,
    Stack,
    aws_s3 as s3,
)
from constructs import Construct
from secure_bucket_construct import SecureBucket
import cdk_nag

class S3BucketWithCfnNagConstructStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        SecureBucket(self, 'SecureS3Bucket')
        Aspects.of(self).add(cdk_nag.AwsSolutionsChecks())

When synthesizing the template you can now see that there is more in the CloudFormation template added, such as an AWS KMS key, S3 Bucket and a S3 Bucket Policy. No errors are raised by the cdk_nag construct. Does it still work? We can double check with cfn_nag_scan.

Now when running cfn_nag_scan on the synthesized template, the outcome is 0 Failures and 0 Warnings:

(.venv) ➜  s3bucket_with_cdk_nag_construct git:(main) ✗ cfn_nag_scan -i cdk.out/S3BucketWithCfnNagConstructStack.template.json
------------------------------------------------------------
cdk.out/S3BucketWithCfnNagConstructStack.template.json
------------------------------------------------------------
Failures count: 0
Warnings count: 0

Happy days!

Recap: with cdk-nag from the Construct Hub we are able to see if resources are being deployed according to best practices during synthesize. It also showed how to use the secure-bucket-construct from pypi.org created in my previous blog.

Try Yourself

The complete code used above can be found in my GitHub: S3 Bucket checked with cdk-nag construct

Did you find this article valuable?

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