This is Chapter 1 of the DevSecOps Guardrails series.
cdk-nag runs against the CDK construct tree at synth time and blocks the build on any rule violation - before a changeset is created, before any AWS API call is made. I wired it in as the first guardrail because it is CDK-native: it sees the construct level, not just the synthesised template, which means it can catch things cfn-lint cannot.
Adding it is a five-line change to app.py. The part that matters more than setup is suppressions: knowing when to suppress versus fix, and writing a reason string that makes the decision visible in a code review. A synth failure maps directly to a blocked pipeline stage in Jenkins, GitHub Actions, and Azure DevOps with no extra configuration needed.
The examples use the S3 + Lambda stack from the Infrastructure as Code with AWS CDK series.
What cdk-nag checks
cdk-nag is an open-source library from cdklabs that runs rule sets (called NagPacks) against a synthesised CDK app. It traverses the CDK construct tree and raises a finding for any resource that violates a rule.
Two severity levels:
- NagError - blocks synthesis. The
cdk synthcommand fails and no CloudFormation template is written until the violation is resolved or suppressed with a justification. - NagWarning - does not block synthesis. Worth reviewing but will not stop a deploy.
NagPacks that ship out of the box: AwsSolutionsChecks, HIPAASecurityChecks, NIST80053R5Checks, PCIDSS321Checks. For most CDK projects, AwsSolutionsChecks is the right starting point - it maps to AWS best practice guidance and covers the most common misconfiguration patterns.
Installation
pip install cdk-nag
Add cdk-nag to requirements.txt so it is installed in CI:
pip freeze | grep cdk-nag >> requirements.txt
Adding cdk-nag to the app
Apply cdk-nag as an Aspect in app.py, after the stacks are instantiated:
import aws_cdk as cdk
from aws_cdk import Aspects
from cdk_nag import AwsSolutionsChecks
from my_cdk_app.my_stack import MyStack
app = cdk.App()
env = app.node.try_get_context("env") or "dev"
config = app.node.try_get_context(env)
MyStack(app, "MyStack",
env=cdk.Environment(account="123456789012", region="us-east-1"),
config=config
)
Aspects.of(app).add(AwsSolutionsChecks())
app.synth()
Every subsequent cdk synth run - locally or in CI - will check the synthesised template against the AwsSolutions rules.
Using additional NagPacks
The same Aspects.of(app).add() pattern applies to all four NagPacks. Add them after AwsSolutionsChecks - once those findings are clean, layering in a compliance-specific pack is low friction.
from cdk_nag import AwsSolutionsChecks, HIPAASecurityChecks, NIST80053R5Checks, PCIDSS321Checks
Aspects.of(app).add(AwsSolutionsChecks())
Aspects.of(app).add(HIPAASecurityChecks()) # HIPAA Security Rule
Aspects.of(app).add(NIST80053R5Checks()) # NIST 800-53 Rev 5
Aspects.of(app).add(PCIDSS321Checks()) # PCI DSS 3.2.1
Each pack runs independently and has its own rule IDs (e.g. HIPAA.Security-S3BucketLoggingEnabled, NIST.800.53.R5-S3BucketLoggingEnabled). A suppression for AwsSolutions-S1 does not suppress the equivalent rule in HIPAA or NIST - if the same resource violates rules across multiple packs, each needs its own suppression with a justification.
Start with AwsSolutionsChecks on its own. Running all four packs simultaneously on a codebase that has not been through any nag checks yet produces a lot of noise and makes it harder to prioritise.
How violations look
Running cdk synth with two common issues in the stack:
[Error at /MyStack/FilesBucket/Resource] AwsSolutions-S1: The S3 bucket does not have server access logging enabled.
[Warning at /MyStack/ProcessorFunction/Resource] AwsSolutions-L1: The non-container Lambda function is not configured to use the latest runtime version.
Found errors
cdk-nag prints the construct path, the rule ID, and a description. The construct path maps directly to how the resource is defined in the stack - /MyStack/FilesBucket/Resource is the s3.Bucket created as FilesBucket inside MyStack.
The Found errors line at the end is what fails the synth. Warnings appear but do not fail it.
Fixing violations
S3 access logging (AwsSolutions-S1)
log_bucket = s3.Bucket(self, "LogsBucket",
removal_policy=RemovalPolicy.DESTROY
)
bucket = s3.Bucket(self, "FilesBucket",
bucket_name=config["bucket_name"],
server_access_logs_bucket=log_bucket
)
Lambda runtime (AwsSolutions-L1)
The L1 warning is about using a pinned runtime version rather than the CDK-managed PYTHON_3_12 value. It resolves by using Runtime.PYTHON_3_12 explicitly, which already is what the series uses. If the warning still fires, it is a signal to check whether the runtime version is current.
Suppressions
Sometimes a rule genuinely does not apply - a development bucket that intentionally has no access logging, a Lambda that has a pinned runtime for a documented reason. Suppress the rule inline with a justification string:
from cdk_nag import NagSuppressions
NagSuppressions.add_resource_suppressions(fn, [
{
"id": "AwsSolutions-L1",
"reason": "Runtime pinned to 3.12; upgrade tracked in backlog"
}
])
The suppression lives in the same stack code as the resource it covers. It shows up in the cdk.out report. That means it is code-reviewed alongside the change that triggered it - not buried in a config file or applied globally.
For a stack-wide suppression (use sparingly):
NagSuppressions.add_stack_suppressions(self, [
{
"id": "AwsSolutions-IAM4",
"reason": "AWSLambdaBasicExecutionRole is an AWS-managed policy; risk accepted"
}
])
cdk-nag in Jenkins, GitHub Actions, and Azure DevOps
Because cdk-nag is applied as an Aspect in app.py, it runs automatically on every cdk synth call - wherever that call happens. The synth step already exists in the Jenkins and GitHub Actions pipelines from Chapter 5 of the CDK series; Azure DevOps follows the same pattern. Adding cdk-nag to requirements.txt is all the pipeline change needed.
Jenkins
stage('Synth') {
steps {
sh 'cdk synth -c env=dev'
}
}
stage('Deploy dev') {
steps {
sh 'cdk deploy --require-approval never -c env=dev'
}
}
When cdk synth runs, cdk-nag runs with it. A NagError causes cdk synth to exit non-zero - Jenkins marks the Synth stage as failed and the Deploy stage never runs. Nothing ships until the violation is fixed or suppressed.
GitHub Actions
- name: Synth
run: cdk synth -c env=dev
- name: Deploy dev
run: cdk deploy --require-approval never -c env=dev
Same behaviour - the Synth step fails the job, which blocks the deploy job downstream (via needs: deploy-dev in the full workflow from Chapter 5).
Azure DevOps
- script: cdk synth -c env=dev
displayName: Synth
- script: cdk deploy --require-approval never -c env=dev
displayName: Deploy dev
A NagError causes the Synth step to exit non-zero - Azure DevOps marks the step as failed and stops the pipeline. The Deploy step never runs.
In all three cases the pipeline stage sequence is:
- Install dependencies
- Synth - cdk-nag runs here; a
NagErrorfails this step and stops the pipeline - Deploy - only runs if step 2 passes
Generating a report
To write cdk-nag findings to a file instead of (or in addition to) stderr, pass verbose and set an output directory:
Aspects.of(app).add(AwsSolutionsChecks(verbose=True))
cdk-nag writes CSV reports per stack to cdk.out/ - one for NagErrors, one for NagWarnings, one for suppressions. Useful for audits.
Notes
- Apply cdk-nag in
app.py, not inside a stack. Applied at the app level it covers every stack in the app. Applied inside a stack it only covers that stack. - Start with
AwsSolutionsChecksand add other NagPacks once the AwsSolutions findings are clean - running all packs at once on a new codebase produces a lot of noise. - A suppression without a
reasonstring will not be accepted by cdk-nag. The reason field is required and shows up in the audit report. - cdk-nag does not check runtime behaviour - it checks the synthesised CloudFormation template. It catches misconfiguration, not application bugs.