CDK Nag: Security and Compliance Testing for AWS CDK Infrastructure
Writing CDK constructs that deploy working infrastructure is the easy part. Writing CDK constructs that deploy secure infrastructure — that's where teams struggle. CDK Nag is a static analysis tool that checks your CDK stacks against security and compliance rules before a single resource is deployed.
This post covers how to integrate CDK Nag into your testing pipeline and enforce infrastructure security as code.
What Is CDK Nag?
CDK Nag is an open-source library from AWS Labs that applies rule packs to your CDK stacks during synthesis. It catches common misconfigurations:
- S3 buckets without encryption or versioning
- Security groups with 0.0.0.0/0 ingress
- RDS instances without deletion protection
- IAM policies with wildcard actions
- Lambda functions without VPC configuration
- Unencrypted EBS volumes
CDK Nag integrates directly with CDK's aspect system — no separate scanning step required.
Installation
npm install cdk-nag
# or
yarn add cdk-nagBasic Setup
Add CDK Nag to your CDK app entry point:
// bin/my-app.ts
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';
import { MyStack } from '../lib/my-stack';
const app = new App();
const stack = new MyStack(app, 'MyStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' }
});
// Apply AWS Solutions rule pack
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));Now run cdk synth. If any rules fire, synthesis fails with detailed error messages:
[Error at /MyStack/MyBucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled.
[Error at /MyStack/MyBucket/Resource] AwsSolutions-S2: The S3 Bucket does not have public access block controls enabled.
[Warning at /MyStack/MyBucket/Resource] AwsSolutions-S10: The S3 Bucket does not require requests to use SSL.Available Rule Packs
CDK Nag ships with several compliance-mapped rule packs:
| Pack | Standard | Use Case |
|---|---|---|
AwsSolutionsChecks |
AWS Solutions | General best practices |
NIST80053R4Checks |
NIST 800-53 Rev 4 | US government workloads |
NIST80053R5Checks |
NIST 800-53 Rev 5 | Updated US government |
HIPAA SecurityChecks |
HIPAA | Healthcare workloads |
PCIDSS321Checks |
PCI DSS 3.2.1 | Payment card industry |
import {
AwsSolutionsChecks,
NIST80053R5Checks,
HIPAASecurityChecks
} from 'cdk-nag';
// Apply multiple packs
Aspects.of(app).add(new AwsSolutionsChecks());
Aspects.of(app).add(new NIST80053R5Checks({ verbose: true }));Writing CDK Nag Tests with Jest
For CI, you want CDK Nag violations to fail your test suite explicitly. Use Jest with the CDK Template and CDK Nag annotations:
// test/my-stack.nag.test.ts
import { App, Aspects } from 'aws-cdk-lib';
import { Annotations, Match } from 'aws-cdk-lib/assertions';
import { AwsSolutionsChecks } from 'cdk-nag';
import { MyStack } from '../lib/my-stack';
describe('CDK Nag Security Tests', () => {
let app: App;
let stack: MyStack;
beforeEach(() => {
app = new App();
stack = new MyStack(app, 'TestStack', {
env: { account: '123456789012', region: 'us-east-1' }
});
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
// Must synth to trigger nag checks
app.synth();
});
test('no CDK Nag errors', () => {
const errors = Annotations.fromStack(stack).findError(
'*',
Match.stringLikeRegexp('AwsSolutions-.*')
);
expect(errors).toHaveLength(0);
});
test('no critical security warnings', () => {
const warnings = Annotations.fromStack(stack).findWarning(
'*',
Match.stringLikeRegexp('AwsSolutions-.*')
);
// Warnings are acceptable — errors are not
// But log them for review
if (warnings.length > 0) {
console.warn('CDK Nag warnings:', warnings.map(w => w.entry.data));
}
});
test('S3 bucket is properly secured', () => {
// Test no S3-specific nag errors
const s3Errors = Annotations.fromStack(stack).findError(
'*',
Match.stringLikeRegexp('AwsSolutions-S.*')
);
expect(s3Errors).toHaveLength(0);
});
test('IAM policies are least-privilege', () => {
const iamErrors = Annotations.fromStack(stack).findError(
'*',
Match.stringLikeRegexp('AwsSolutions-IAM.*')
);
expect(iamErrors).toHaveLength(0);
});
});Suppressing Rules with Justification
Not every rule applies to every situation. CDK Nag lets you suppress specific rules with a required reason — creating an audit trail of intentional exceptions:
import { NagSuppressions } from 'cdk-nag';
// Suppress a rule on a specific construct
const bucket = new s3.Bucket(this, 'DataBucket', {
// ... config
});
NagSuppressions.addResourceSuppressions(bucket, [
{
id: 'AwsSolutions-S1',
reason: 'This is a temporary scratch bucket used only for Lambda ephemeral processing. Access logs are not required per our data classification policy for scratch data.'
}
]);
// Suppress on a construct and all children
NagSuppressions.addResourceSuppressions(
myLambdaRole,
[
{
id: 'AwsSolutions-IAM4',
reason: 'AWSLambdaBasicExecutionRole is acceptable for this read-only Lambda per security review 2025-Q3'
}
],
true // applyToChildren
);
// Suppress at the stack level
NagSuppressions.addStackSuppressions(stack, [
{
id: 'AwsSolutions-IAM5',
reason: 'Wildcard actions on CloudWatch Logs are acceptable per our log writing policy'
}
]);Testing That Suppressions Are Documented
You can test that suppressions include justification — preventing lazy reason: 'suppressed' entries:
test('all suppressions have meaningful justifications', () => {
// Read the suppression registry from your stack
const suppressions = getNagSuppressions(stack);
for (const suppression of suppressions) {
expect(suppression.reason.length).toBeGreaterThan(20);
expect(suppression.reason).not.toMatch(/suppressed|ignore|skip|n\/a/i);
}
});Custom Rules
CDK Nag's built-in rules don't cover every organizational policy. Write custom rules for your specific requirements:
// lib/nag-rules/require-cost-tags.ts
import { CfnResource, Stack } from 'aws-cdk-lib';
import { NagMessageLevel, NagPack, NagPackProps } from 'cdk-nag';
export class CompanyPolicyChecks extends NagPack {
constructor(props?: NagPackProps) {
super(props);
this.packName = 'CompanyPolicy';
}
public visit(node: IConstruct): void {
if (node instanceof CfnResource) {
this.checkRequiredTags(node);
this.checkResourceNaming(node);
}
}
private checkRequiredTags(resource: CfnResource): void {
const requiredTags = ['CostCenter', 'Environment', 'Owner'];
const resourceTags = (resource as any).tags?.renderTags() || {};
for (const tag of requiredTags) {
if (!resourceTags[tag]) {
this.applyRule({
ruleSuffixOverride: 'TAG001',
info: `Resource is missing required tag: ${tag}`,
explanation: 'All resources must have CostCenter, Environment, and Owner tags per company tagging policy.',
level: NagMessageLevel.ERROR,
rule: class extends NagRules {
static readonly [$RuleMetaData] = {
info: `Missing required tag: ${tag}`,
explanation: 'Required for cost allocation and ownership tracking'
};
public validate(node: CfnResource): boolean {
return false; // already checked above
}
},
node: resource
});
}
}
}
private checkResourceNaming(resource: CfnResource): void {
// Enforce kebab-case naming for logical IDs
const logicalId = Stack.of(resource).getLogicalId(resource);
if (!/^[a-z][a-z0-9-]*$/.test(logicalId)) {
this.applyRule({
// ... rule config
});
}
}
}Use it alongside standard packs:
Aspects.of(app).add(new AwsSolutionsChecks());
Aspects.of(app).add(new CompanyPolicyChecks({ verbose: true }));CI/CD Integration
CDK Nag checks should block deployment when violations exist. In GitHub Actions:
# .github/workflows/cdk-security.yml
name: CDK Security Checks
on:
pull_request:
paths:
- 'infra/**'
- 'lib/**'
jobs:
cdk-nag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run CDK Nag tests
run: npm test -- --testPathPattern="nag"
env:
CDK_DEFAULT_ACCOUNT: '123456789012'
CDK_DEFAULT_REGION: 'us-east-1'
- name: CDK Synth with Nag (additional check)
run: npx cdk synth 2>&1 | tee synth-output.txt
- name: Check for Nag errors in synth output
run: |
if grep -q '\[Error' synth-output.txt; then
echo "❌ CDK Nag violations found:"
grep '\[Error' synth-output.txt
exit 1
fi
echo "✅ No CDK Nag errors"Combining CDK Nag with Fine-Grained Assertions
CDK Nag and CDK's Template.fromStack assertions are complementary. Use both:
import { Template } from 'aws-cdk-lib/assertions';
import { Annotations, Match } from 'aws-cdk-lib/assertions';
import { AwsSolutionsChecks } from 'cdk-nag';
describe('Database Stack', () => {
let template: Template;
let stack: DatabaseStack;
beforeEach(() => {
const app = new App();
stack = new DatabaseStack(app, 'DBStack', { /* props */ });
Aspects.of(app).add(new AwsSolutionsChecks());
app.synth();
template = Template.fromStack(stack);
});
// Fine-grained assertions: check the structure is correct
test('RDS instance has correct config', () => {
template.hasResourceProperties('AWS::RDS::DBInstance', {
DBInstanceClass: 'db.t3.medium',
MultiAZ: true,
StorageEncrypted: true
});
});
// CDK Nag: check compliance rules pass
test('no RDS compliance violations', () => {
const rdsErrors = Annotations.fromStack(stack).findError(
'*',
Match.stringLikeRegexp('AwsSolutions-RDS.*')
);
expect(rdsErrors).toHaveLength(0);
});
});Metrics and Tracking Compliance Over Time
Export CDK Nag results as JSON for tracking compliance drift over time:
// tools/nag-report.ts
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks, NagReportFormat, NagReportLogger } from 'cdk-nag';
const app = new App();
// ... add stacks
Aspects.of(app).add(new AwsSolutionsChecks({
reports: true, // Generate CSV reports
verbose: true
}));
app.synth();
// Reports written to cdk.out/AwsSolutions-*.csvCommit the CSV report and diff it in CI — a great way to see whether your compliance posture is improving:
git diff --stat HEAD~1 HEAD -- "cdk.out/*.csv"Key Takeaways
CDK Nag shifts security-left in your infrastructure pipeline:
- Run it on
cdk synthand in Jest tests — catches issues before any AWS API call - Use
NagSuppressionswith meaningful reasons to create an auditable exception log - Write custom rules for company-specific policies
- Pair with fine-grained
Templateassertions for complete coverage - Export reports for compliance trending over time
For teams running HelpMeTest against CDK-provisioned environments, CDK Nag ensures the infrastructure your tests run on is itself compliant — closing the loop between application testing and infrastructure security.