DevSecOps

AWS CDK Construct Library Security

CDK constructs are code that provisions infrastructure. Most teams audit the infrastructure but not the constructs. Here is how to think about construct library security and what to check.

Nayan Dey
Senior Security Engineer
7 min read

CDK is code that writes CloudFormation. Most security teams review the CloudFormation output, or the deployed resources, and assume the CDK code that produced them is trustworthy because it is "just configuration." It is not. CDK construct libraries are npm packages, or PyPI packages, or Maven artifacts, that execute arbitrary code at synth time. They pull dependencies. They do HTTP requests. They read environment variables. Treating them as benign configuration is the same category mistake as treating a webpack plugin as benign.

This post is about how to actually audit the construct libraries you use, what synth-time behavior looks like, and the patterns I see introducing real risk in production CDK stacks.

What a construct actually does

When you call new SomeConstruct(this, 'id', props) in your CDK code, you are instantiating a JavaScript class (or Python, Java, etc.) that runs immediately. The constructor can do anything: read files from disk, hit external APIs, shell out to processes, generate Lambda function code on the fly, fetch secrets from environment variables. The output is a tree of CloudFormation resources, but the path from input to output is arbitrary code execution on the synth machine.

This means a malicious construct can exfiltrate secrets from your CI environment, inject resources into your CloudFormation template that your CDK code did not explicitly request, or make synth-time network calls that reveal which accounts and regions you deploy to. None of this is hypothetical. All of it is easy to implement and hard to detect.

The synth machine in most organizations is a CodeBuild project or a GitHub Actions runner with broad AWS permissions. A construct library that runs code at synth time runs in that context. If the library is compromised, your synth environment is compromised.

The aws-cdk-lib distinction

AWS publishes aws-cdk-lib as a single monolithic package that contains all the first-party L1 and L2 constructs. This is first-party AWS code, published from an AWS-owned npm account, and it is the lowest-risk construct library you can use. Stick with it where possible.

The risk increases as you move outward:

  1. First-party AWS constructs (aws-cdk-lib, @aws-cdk/aws-lambda-nodejs): AWS-signed, AWS-reviewed, and the provenance is clear.
  2. AWS Solutions constructs (@aws-solutions-constructs/*): AWS-owned but a separate team with a different release cadence. Slightly higher risk because the review processes are less visible.
  3. Construct Hub community libraries: Third-party packages published by individuals or companies, listed on constructs.dev. Provenance varies widely. Some are excellent. Some are abandoned. Some are effectively unreviewed.
  4. Private internal constructs: Libraries built inside your organization. The risk here depends entirely on your internal publishing controls, which are often weaker than you would like.

Most CDK incidents I have worked on involved categories 3 or 4.

Dependency chains are worse than they look

A CDK construct library is an npm package. It has dependencies. Those dependencies have dependencies. A typical CDK stack pulls in five to ten construct libraries directly, which transitively pull in two to three hundred npm packages. Every one of those packages is code that runs at synth time.

Run npm ls --all on a CDK project and see what comes back. I have seen CDK projects that pull in request, debug, chalk, and fifty other packages that have had supply chain incidents in the last five years. Those packages do not have to be CDK-aware to be dangerous. They just have to be imported somewhere in the dependency tree and have their module-load code execute malicious behavior.

The mitigation:

  • Lock the dependency tree with a committed package-lock.json or pnpm-lock.yaml, and enforce in CI that lockfile changes require code review.
  • Run an SCA scan on the full synth-time dependency tree, not just the production dependencies of your application code. The CDK dependencies are production dependencies of your infrastructure, even if your application code does not import them.
  • Pin the CDK version in package.json to an exact version, not a caret range. aws-cdk-lib: "2.143.0" not aws-cdk-lib: "^2.143.0". Updates are deliberate.

Synth-time network calls

A useful audit technique: run cdk synth in a sandboxed environment with egress blocked except to known-good destinations, and log every network call that fails. In a clean CDK project there should be essentially none. In a typical CDK project there are several, and they are worth investigating.

Common offenders:

  • Construct libraries that fetch Lambda runtime images at synth time to generate bundled assets. This is often legitimate (the aws-cdk-lib/aws-lambda-nodejs bundling behavior, for example) but should be understood and pinned.
  • Libraries that hit GitHub APIs to fetch versions of upstream tools. Usually a bad pattern; the version should be pinned in the library, not fetched on every synth.
  • Libraries that make telemetry calls. Most CDK libraries do not do this. If you see a library calling a telemetry endpoint at synth time, decide whether you accept that.
  • Libraries that resolve AWS account information by calling sts:GetCallerIdentity. This is legitimate for some constructs that need to embed the account ID into a template, but the calls should be explicit and documented.

Custom resources and the lambda-within-lambda problem

Many CDK constructs use AWS CloudFormation custom resources implemented as Lambda functions that the construct bundles. When you deploy the stack, CloudFormation creates a Lambda function whose code came from the construct library, and invokes it as part of the deployment. That Lambda has an execution role granted by the construct, which is often broader than it needs to be.

The audit: for every custom resource in your CDK stack, look at the Lambda function code that the construct ships, and look at the IAM role the construct requests. I have found constructs that ship custom resources with iam:* on Resource: "*" because it was simpler for the construct author to write. The construct does three things; the role allows a thousand. If the construct is compromised, the custom resource Lambda runs with that role in your production account.

Signed construct libraries, eventually

npm has been rolling out provenance attestations via Sigstore. CDK first-party libraries publish with provenance as of late 2023. You can verify the provenance by running npm audit signatures in your CDK project, which checks that each installed package has a valid Sigstore attestation from its registered publisher.

This is not yet a complete story (many community construct libraries do not publish with provenance), but it is the right direction. Require provenance for first-party AWS packages in your CI. As community library provenance coverage improves, tighten the requirement.

The control I actually recommend

Treat CDK code exactly like application code. Same code review process, same SCA scans, same branch protection, same deploy pipeline. Do not have a parallel "infra" path with weaker controls because the code is "just provisioning." The code is not just provisioning. It is arbitrary code that runs in a privileged environment and produces CloudFormation that creates resources in your production accounts. It is often the highest-privilege code your organization runs.

How Safeguard Helps

Safeguard scans CDK projects for the synth-time dependency tree, not just the application dependencies, and flags construct libraries with missing provenance, stale maintainers, or recent incident history. We track which constructs your organization uses across repositories and alert on newly introduced constructs from unfamiliar publishers. For custom-resource-heavy constructs, Safeguard analyzes the bundled Lambda function code and the IAM roles the construct requests, so you see the actual privilege of the provisioning path before the stack is deployed.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.