Skip to content

How to Control Security Groups in AWS CDK Custom Resource Lambda Functions

4 minute read
Content level: Advanced
1

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:

  1. Create Security Group - Include only necessary rules
  2. Create Lambda Function - Explicitly specify security groups
  3. Create Provider - Use Lambda function as onEventHandler [5]
  4. 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