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