Back to Blog

4 minutes read

Nagging can be good: Improving your AWS IaC with cdk-nag

Jovica Zorić

Chief Technology Officer

The AWS Cloud Development Kit (CDK) allows engineers to define and deploy cloud resources using code, enabling fast, repeatable, and consistent infrastructure setups. But while CDK simplifies resource creation, it doesn’t automatically enforce security, compliance, or best practices.

Enter cdk-nag, your infrastructure reviewer. It identifies security gaps, compliance violations, and deviations from best practices before they become problems. cdk-nag can check your code against predefined rule sets—such as AWS Solutions, HIPAA, NIST, or PCI—helping you build smarter and safer cloud environments. It is a part of the AWS CDK ecosystem and maintained by AWS itself.

Let’s see it in action by working on a common use case: setting up a simple workshop environment with a VPC, EC2 instance, and security groups. Here’s the starting AWS CDK code in TypeScript (a Golang version is also available (4)).

// ####### START VPC #######
const vpc = new Vpc(this, 'workshop-vpc', {
    natGateways: 0,
    maxAzs: 1,
    subnetConfiguration: [
        {
            cidrMask: 24,
            name: 'workshop-public',
            subnetType: SubnetType.PUBLIC,
            mapPublicIpOnLaunch: true,
        }
    ],
});
// ####### END VPC #######

// ####### START SSH SG #######
const sshSecurityGroup = new SecurityGroup(this, 'workshop-ssh-sg', {
    vpc: vpc,
    description: 'Workshop ssh security group',
    allowAllOutbound: true,
})
sshSecurityGroup.addIngressRule(
    Peer.anyIpv4(),
    Port.tcp(22),
    'SSH from everywhere'
)
// ####### END SSH SG #######

// ####### START EC2 ROLE #######
// This role will allow the instance to put log events to CloudWatch Logs
const ec2Role = new Role(this, 'workshop-ec2-role', {
    assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
    inlinePolicies: {
        ['RetentionPolicy']: new PolicyDocument({
            statements: [
                new PolicyStatement({
                    resources: ['*'],
                    actions: ['logs:PutRetentionPolicy'],
                }),
            ],
        }),
    },
    managedPolicies: [
        ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'),
    ],
});
// ####### END EC2 ROLE #######

// ####### START APP SG #######
const appSecurityGroup = new SecurityGroup(this, 'workshop-app-sg', {
        vpc: vpc,
        description: "Workshop app security group",
        allowAllOutbound: true,
    }
)
appSecurityGroup.addIngressRule(
    Peer.anyIpv4(),
    Port.tcp(8085),
    "Our APP will be running on this port"
);
// ####### END APP SG #######

// ####### START EC2 INSTANCE ######
const ec2Instance = new Instance(this, 'workshop-ec2-instance', {
    vpc: vpc,
    instanceType: InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.MEDIUM),
    machineImage: MachineImage.genericLinux({
        "eu-central-1": 'ami-0cee4a3eca5195216',
    }, {}),
    securityGroup: appSecurityGroup,
    role: ec2Role,
    userData: UserData.forLinux(),
    blockDevices: [{
        deviceName: "/dev/xvdh",
        volume: BlockDeviceVolume.ebs(8, {
            encrypted: true
        })
    }],
})

ec2Instance.userData.addCommands(
    `echo "${props.sshPubKey}" >> /home/ubuntu/.ssh/authorized_keys`,
);

ec2Instance.addSecurityGroup(sshSecurityGroup);

new CfnOutput(this, 'ssh-command', {
    value: `ssh -i live ubuntu@${ec2Instance.instancePublicDnsName}`
});
// ####### END EC2 INSTANCE ######

This is fairly standard code you’ll encounter in many examples. We’ve been discussing best practices and how to implement them effectively, so we decided to integrate cdk-nag to analyze our infrastructure code and understand its recommendations. 

First things first, adding cdk-nag to our project.

Adding cdk-nag to our project

The first step is to install the cdk-nag package in your project.

npm install cdk-nag

To start, we added cdk-nag to the whole stack by modifying bin/nagging-can-be-good.ts (1):

Aspects.of(app).add(new AwsSolutionsChecks());

We want to check our IaC against best practices based on the AWS Solutions Security Matrix. Check out other rules here: https://github.com/cdklabs/cdk-nag?tab=readme-ov-file#available-rules-and-packs 

Now that the setup is complete we can run `cdk synth` and review each issue and its resolution.

Issue #1: VPC

[Error at /NaggingCanBeGoodStack/workshop-vpc/Resource] AwsSolutions-VPC7: The VPC does not have an associated Flow Log.

Solution:

To fix this error, we can enable VPC Flow logs and direct all logs to CloudWatch. While this will increase network traffic visibility, CloudWatch integration is fine, but S3 is often a better destination for the following reasons: 

  • lower storage costs, 
  • flexible lifecycle management, 
  • native integration with Athena for log analysis. 

For more details, check the documentation here and pricing here

Let’s update our code:

// after subnet configuration
flowLogs: {
    'cw': {
        destination: FlowLogDestination.toCloudWatchLogs()
    }
}
});

Issue #2: Security Group (SSH)

[Error at /NaggingCanBeGoodStack/workshop-ssh-sg/Resource] AwsSolutions-EC23: The Security Group allows for 0.0.0.0/0 or ::/0 inbound access.

Solution:

To resolve this issue, we should allow SSH connection from known IPs, such as our home or work IPs.

Peer.ipv4("80.81.82.83/32")

For the workshop, we will acknowledge the risk and suppress the error by adding a `NagSuppressions:`

NagSuppressions.addResourceSuppressions(sshSecurityGroup, [
    {id: 'AwsSolutions-EC23', reason: 'Can be open as workshop will be done in 1h.'}
]);

Issue #3: EC2 Role

[Error at /NaggingCanBeGoodStack/workshop-ec2-role/Resource] AwsSolutions-IAM4[Policy::arn:<:partition>:iam::aws:policy/CloudWatchAgentServerPolicy]: The IAM user, role, or group uses AWS managed policies.

Solution:

An AWS managed policy is a standalone policy that is created and administered by AWS. Currently, many AWS managed policies do not restrict resource scope. We can replace AWS managed policies with our custom managed ones, but for the workshop we will suppress this error by adding NagSuppressions.

[Error at /NaggingCanBeGoodStack/workshop-ec2-role/Resource] AwsSolutions-IAM5[Resource::*]: The IAM entity contains wildcard permissions and does not have a cdk-nag rule suppression with evidence for those permission.

Solution:

We should keep in mind that overly permissive IAM policies using wildcards (*) are not recommended and should be avoided. For this workshop, it’s fine, we can suppress it. 

To group everything together:

NagSuppressions.addResourceSuppressions(ec2Role, [
    {
        id: 'AwsSolutions-IAM4',
        reason: 'It is ok for our workshop'
    },
    {
        id: 'AwsSolutions-IAM5',
        reason: 'It is ok for our workshop',
        appliesTo: ['Resource::*']
    }
])

Issue #4: Security Group (APP)

The APP Security Group flagged the same issue as the SSH group (0.0.0.0/0 access). We will use the same solution as in Issue #2.

Issue #5: EC2 Instance

[Error at /NaggingCanBeGoodStack/workshop-ec2-instance/Resource] AwsSolutions-EC26: The resource creates one or more EBS volumes that have encryption disabled.

Solution:

That’s a good point. Let’s turn on encryption for our EBS volume, which will help protect data at rest.

blockDevices: [{
    deviceName: "/dev/xvdh",
    volume: BlockDeviceVolume.ebs(8, {
        encrypted: true
    })
}]

[Error at /NaggingCanBeGoodStack/workshop-ec2-instance/Resource] AwsSolutions-EC28: The EC2 instance/AutoScaling launch configuration does not have detailed monitoring enabled.

Solution:

Our instance is configured with default monitoring, and for our workshop, we don’t need detailed monitoring. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/manage-detailed-monitoring.html 

Let’s suppress it:

NagSuppressions.addResourceSuppressions(ec2Instance, [
    {id: 'AwsSolutions-EC28', reason: 'Basic monitoring is enough for this workshop'}
]);

[Error at /NaggingCanBeGoodStack/workshop-ec2-instance/Resource] AwsSolutions-EC29: The EC2 instance is not part of an ASG and has Termination Protection disabled.

Solution:

For this workshop, we don’t need to think about Termination Protection, as the environment will be destroyed in 1 hour.

Let’s suppress it:

NagSuppressions.addResourceSuppressions(ec2Instance, [
    {id: 'AwsSolutions-EC29', reason: 'No termination protection needed for this workshop'},
]);

In the end

Integrating cdk-nag into your AWS CDK projects is more than just checking boxes—it’s about building a robust foundation for your cloud infrastructure. While the initial warnings might seem overwhelming, each resolution strengthens your architecture and helps prevent potential security incidents. Suppressing rules is sometimes necessary, and that’s ok, but ensure each override is thoroughly documented to maintain transparency and accountability.

You can also create your own NagPack, which is outside the scope of this blog post, but be sure to check out the docs: https://github.com/cdklabs/cdk-nag/blob/main/docs/NagPack.md 

Check out the cdk-nag documentation, and let me finish by saying It’s better to be nagged than being alerted!

Resources

Tags:


Jovica Zorić

Chief Technology Officer

Jovica is a techie with more than ten years of experience. His job has evolved throughout the years, leading to his present position as the CTO at ProductDock. He will help you to choose the right technology for your business ideas and focus on the outcomes.


Related posts.