When managing infrastructure as code with AWS CDK, you may need to directly control security groups for Lambda functions created through AwsCustomResource. In environments with strict security policies, the automatically generated security group's 0.0.0.0/0 egress rule can pose compliance issues.
This document provides a step-by-step guide on how to work around the limitations of AwsCustomResource to gain full control over security groups.
Overview
When managing infrastructure as code with AWS CDK, you may need to directly control security groups for Lambda functions created through AwsCustomResource. In environments with strict security policies, the automatically generated security group's 0.0.0.0/0 egress rule can pose compliance issues.
This document provides a step-by-step guide on how to work around the limitations of AwsCustomResource to gain full control over security groups.
⚠️ Important: The code examples in this document were written in a test environment. Please conduct thorough code reviews and testing before applying them to production environments.
Problem Statement
AwsCustomResource supports VPC and subnet configuration but does not provide an option to directly specify security groups [1] [2]. As a result, automatically created security groups include rules that allow all outbound traffic, which can violate security policies.
Solution
Instead of using AwsCustomResource, you can use Provider and CustomResource directly to gain complete control over security groups. This approach consists of the following steps:
- Create Security Group - Include only necessary rules
- Create Lambda Function - Explicitly specify security groups
- Create Provider - Use Lambda function as onEventHandler [5]
- Create CustomResource - Use Provider's serviceToken
Step 1: Create Security Group and Configure Rules
First, create a security group and add only the necessary rules. The most important aspect is setting allowAllOutbound: false to remove the default 0.0.0.0/0 egress rule [3].
// Reference existing VPC
const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: 'vpc-abcdefg' });
// Create security group
const customResourceLambdaSg = new ec2.SecurityGroup(
this,
'CustomResourceLambdaSG',
{
vpc: vpc,
description: 'Security group for Custom Resource Lambda',
allowAllOutbound: false, // Important: Remove default 0.0.0.0/0 rule
}
);
// Inbound rule: Allow HTTPS access only from within VPC
customResourceLambdaSg.addIngressRule(
ec2.Peer.ipv4(vpc.vpcCidrBlock),
ec2.Port.tcp(443),
'Allow HTTPS from VPC'
);
// Outbound rule: Allow only HTTPS for AWS API calls
customResourceLambdaSg.addEgressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'AWS API calls'
);
Explanation:
- Setting
allowAllOutbound: false prevents the default egress rule from being created.
- Use
addIngressRule and addEgressRule to explicitly add only necessary rules [4].
- The example restricts outbound traffic to HTTPS (443) only, but you can adjust this based on your environment.
Step 2: Create Lambda Function (Specify Security Group)
Create a Lambda function that will serve as the handler for the Custom Resource. This function allows you to explicitly specify security groups.
// Reference target Lambda function to invoke
const targetLambda = lambda.Function.fromFunctionArn(
this,
'TargetLambda',
'arn:aws:lambda:region:account:function:function-name'
);
// Create Custom Resource handler Lambda function
const onEventHandler = new lambda.Function(
this,
'OnEventHandler',
{
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.onEvent',
code: lambda.Code.fromInline(`
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const client = new LambdaClient();
exports.onEvent = async (event) => {
console.log('Event:', JSON.stringify(event, null, 2));
if (event.RequestType === 'Create' || event.RequestType === 'Update') {
const command = new InvokeCommand({
FunctionName: '${targetLambda.functionName}',
InvocationType: 'Event',
Payload: JSON.stringify({
RequestType: event.RequestType,
}),
});
await client.send(command);
}
return {
PhysicalResourceId: 'CustomResourceTrigger',
};
};
`),
vpc: vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [customResourceLambdaSg], // Explicitly specify security group
timeout: cdk.Duration.seconds(120),
}
);
// Grant Lambda execution permissions
onEventHandler.addToRolePolicy(
new iam.PolicyStatement({
actions: ['lambda:InvokeFunction'],
effect: iam.Effect.ALLOW,
resources: [targetLambda.functionArn],
})
);
Explanation:
- Use the
securityGroups property to explicitly specify the security group created earlier.
- The Lambda function code implements the logic from
AwsCustomResource's onCreate, onUpdate, and onDelete.
- Check
event.RequestType to handle Create, Update, and Delete events.
Step 3: Create Provider and CustomResource
Finally, create the Provider and CustomResource to connect them.
// Create Provider
const provider = new cr.Provider(
this,
'CustomResourceProvider',
{
onEventHandler: onEventHandler,
}
);
// Create CustomResource
const customResource = new cdk.CustomResource(
this,
'CustomResourceTrigger',
{
serviceToken: provider.serviceToken,
}
);
Explanation:
- The
Provider uses the Lambda function created earlier as the onEventHandler.
- The
CustomResource invokes the Lambda function through the Provider's serviceToken.
- This structure provides complete control over security groups.
Additional Considerations
VPC Endpoint Configuration
When using private subnets, configure VPC Endpoints to allow Lambda functions to call AWS APIs:
vpc.addInterfaceEndpoint('LambdaEndpoint', {
service: ec2.InterfaceVpcEndpointAwsService.LAMBDA,
});
Customizing Security Group Rules
You can adjust security group rules to match your environment:
- Allow specific IP ranges:
ec2.Peer.ipv4('10.0.0.0/16')
- Allow specific security groups:
ec2.Peer.securityGroupId(sg.securityGroupId)
- Allow specific ports:
ec2.Port.tcp(443), ec2.Port.tcpRange(8080, 8090)
References