← Home

Creating a VPC with CDK

It’s one line

const vpc = new ec2.Vpc(this, "VPC", {})

Why is this a blog post then?

The default configuration is usually not sufficient to pass security audits, since it doesn’t follow AWS best practice.

This post explains what else is required.

Add an isolated subnet

The default configuration for VPCs is sensible. You get 1 public, and 1 private subnet per availability zone in the region, but I add in the isolated subnet.

Typically, only load balancers and NAT Gateways should be placed in the public subnets. Lambda functions, Fargate instances etc., should be in the private subnet, where traffic from the Internet can’t make direct inbound connections.

CDK automatically sets up NAT Gateways, and all of the network routing required to enable private subnets to make outbound connections to the Internet (e.g. to make outbound API calls).

Use the isolated subnet to hold infrastructure that isn’t allowed to connect to the Internet. When combined with VPC endpoints, data held in S3 or DynamoDB can still be processed.

const vpc = new ec2.Vpc(this, "VPC", {
	subnetConfiguration: [
			name: "public-subnet",
			subnetType: ec2.SubnetType.PUBLIC,
			cidrMask: 24,
			name: "private-subnet",
			subnetType: ec2.SubnetType.PRIVATE,
			cidrMask: 24,
			name: "isolated-subnet",
			subnetType: ec2.SubnetType.ISOLATED,
			cidrMask: 24,
	// maxAzs: 2,
	// natGateways: 2,

Consider your CIDR range if you need to use a VPN

If you want to change your range later, you have to delete and recreate the VPC.

If you might need to set up a VPN to another network, contact their team for advice so that your address range doesn’t conflict.

Consider NAT Gateway costs

In my closest region (Ireland), it costs ($0.048 per hour * 24 hours * 365 days a year / 12 months) = $35.04, per availability zone to run a NAT gateway, and by default, you’re likely to need 3.

To control costs, you can reduce the number of zones with the maxAzs config parameter.

In my personal projects, I sometimes place instances in the public subnet and set the natGateways field to zero. This means there’s no running costs for the network, but if I open up an instance to the Internet, it can be attacked easily, since it’s not protected in any way.

Create a bucket for logs

It’s best practice to log API Gateway access, CloudFront requests, VPC flow logs etc.

If you don’t enable logging on these services, it will show up in security audits as a finding, since it’s part of CIS AWS Foundation Benchmarks [1].

To store VPC flow logs, and other logs, you need a log bucket to put them in.

The default CDK configuration doesn’t follow best practice in several areas, and so will be flagged by AWS security tooling, you also need to:

  • Block public access to prevent accidental exposure of data.
  • Ensure the use of TLS to transfer data.
  • Enable versioning.
  • Enable encryption.
  • Archive logs to save money.
const s3LogBucket = new s3.Bucket(this, "s3LogBucket", {
	blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
	enforceSSL: true,
	versioned: true,
	accessControl: s3.BucketAccessControl.LOG_DELIVERY_WRITE,
	encryption: s3.BucketEncryption.S3_MANAGED,
	intelligentTieringConfigurations: [
			name: "archive",
			archiveAccessTierTime: Duration.days(90),
			deepArchiveAccessTierTime: Duration.days(180),

Setup VPC Flow Logs

If your service is attacked, you can use flow logs to determine what outbound or inbound IP addresses were used, and also determine the quantity of data extracted etc.

VPC flow logs are required by security tooling like AWS GuardDuty to look for unusual traffic patterns, so you generally need it switched on.

You may need to copy flow logs up outside of the account, since an attacker might delete them.

const vpcFlowLogRole = new iam.Role(this, "vpcFlowLogRole", {
	assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"),
s3LogBucket.grantWrite(vpcFlowLogRole, "sharedVpcFlowLogs/*")

// Create flow logs to S3.
new ec2.FlowLog(this, "sharedVpcLowLogs", {
	destination: ec2.FlowLogDestination.toS3(s3LogBucket, "sharedVpcFlowLogs/"),
	trafficType: ec2.FlowLogTrafficType.ALL,
	flowLogName: "sharedVpcFlowLogs",
	resourceType: ec2.FlowLogResourceType.fromVpc(vpc),

Use VPC Endpoints

For workloads that use AWS services, VPC endpoints can reduce outbound internet traffic and NAT Gateway costs.

VPC Endpoints improve security, since your data does not mix with public Internet traffic.

CDK sets up all of the required network routing, and the AWS SDK uses the endpoints automatically. No changes are required to program code to benefit from them.

vpc.addGatewayEndpoint("dynamoDBEndpoint", {
	service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
vpc.addGatewayEndpoint("s3Endpoint", {
	service: ec2.GatewayVpcEndpointAwsService.S3,

Create a standard security group for Lambda

Many organisations require that AWS Lambda functions run inside a VPC to benefit from VPC flow logs.

If you just keep adding new Lambda functions to the VPC, eventually, you’ll get the error:

You have exceeded the maximum limit for HyperPlane ENIs for your account

This is because, by default, each Lambda function creates a new security group for itself. This is pointless because the vast majority of the time, the security group is simply the default.

To prevent running out of ENIs, create a default security group and encourage its use.

const noInboundAllOutboundSecurityGroup = new ec2.SecurityGroup(this, "noInboundAllOutboundSecurityGroup", {
	vpc: vpc,
	allowAllOutbound: true,
	description: "No inbound / all outbound",
	securityGroupName: "noInboundAllOutboundSecurityGroup",
new CfnOutput(this, "noInboundAllOutboundSecurityGroup", {
	exportName: "noInboundAllOutboundSecurityGroup",
	value: noInboundAllOutboundSecurityGroup.securityGroupId,

Remove the default security group

Any VPC you create will have default security groups added which allow egress.

These immediately show up in security audits and should be removed, however, CDK can’t modify them, so I wrote a program to remove them [2]


CDK’s default configuration is OK, but won’t pass a security review out of the box, but only a few tweaks are needed to improve things.

Check out the full code at [3]