Last updated: Jan 26, 2024
Reading timeยท11 min

Security groups are virtual firewalls. They control the traffic that goes in and out of our EC2 instances.
They allow us to define inbound and outbound rules. Inbound traffic is traffic that comes into the EC2 instance, whereas Outbound traffic is traffic that goes out of the EC2 instance.
We are going to look at multiple examples of creating security groups and editing their inbound and outbound rules.
Let's start by creating a VPC and a security group for a web server.
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); const vpc = new ec2.Vpc(this, 'my-cdk-vpc', { ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), natGateways: 0, maxAzs: 3, subnetConfiguration: [ { name: 'public-subnet-1', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, ], }); // ๐ Create a SG for a web server const webserverSG = new ec2.SecurityGroup(this, 'web-server-sg', { vpc, allowAllOutbound: true, description: 'security group for a web server', }); webserverSG.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'allow SSH access from anywhere', ); webserverSG.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'allow HTTP traffic from anywhere', ); webserverSG.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'allow HTTPS traffic from anywhere', ); webserverSG.addIngressRule( ec2.Peer.ipv4('123.123.123.123/16'), ec2.Port.allIcmp(), 'allow ICMP traffic from a specific IP range', ); } }
Let's go over what we did in the code sample:
We created a VPC, by instantiating and configuring the Vpc class. We
set the natGateways prop to 0 to avoid getting charged unnecessarily.
If you want to read more about creating VPCs, I've written another article on creating VPCs in CDK.
We created a security group for a web server. The props we passed when instantiating the SecurityGroup class are:
vpc - the VPC, the security group will be created inallowAllOutbound - whether the security group should allow all outbound
traffic. By default allowAllOutbound is set to truedescription - a short description of the security groupSecurityGroup class. The parameters we passed
to the method are:peer - the Source in a security group inbound ruleconnection - the Port, Protocol and Type in a security group inbound
ruledescription - a short description of the security group rule| Type | Protocol | Port | Source |
|---|---|---|---|
| SSH | TCP | 22 | 0.0.0.0/0 |
| HTTP | TCP | 80 | 0.0.0.0/0 |
| HTTPS | TCP | 443 | 0.0.0.0/0 |
| All ICMP | ICMP | ALL | 123.123.0.0/16 |
Let's provision the VPC and the security group:
npx aws-cdk deploy
After the resources have been deployed, we can see that the inbound security group rules have been applied:

The outbound rules allow all traffic because we've set the allowAllOutbound
to true, which is also the default value:

Let's add 2 more security groups - 1 for a backend server, and 1 for a database server.
The security group of the backend server will only allow requests on port
8000, made from instances in the webserverSG security group.
Whereas the security group for the database server will only allow requests on
port 3306, made from instances in the backendServerSG security group.
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐ Create a SG for a backend server const backendServerSG = new ec2.SecurityGroup(this, 'backend-server-sg', { vpc, allowAllOutbound: true, description: 'security group for a backend server', }); backendServerSG.connections.allowFrom( new ec2.Connections({ securityGroups: [webserverSG], }), ec2.Port.tcp(8000), 'allow traffic on port 8000 from the webserver security group', ); // ๐ Create a SG for a database server const dbserverSG = new ec2.SecurityGroup(this, 'database-server-sg', { vpc, allowAllOutbound: true, description: 'security group for a database server', }); dbserverSG.connections.allowFrom( new ec2.Connections({ securityGroups: [backendServerSG], }), ec2.Port.tcp(3306), 'allow traffic on port 3306 from the backend server security group', ); } }
Let's go over what we did in the code sample:
8000 from instances in the
webserverSG security group. The allowFrom method takes the following 3
props:connectable - an object that has connection options, in our case a security
group
connection - the Port, Protocol and Type in a security group
rule
description - a short description of the security group rule
allowFrom method
to allow traffic on port 3306 from the security group of the backend server.The inbound rules for the backendServerSG look as follows:
| Type | Protocol | Port | Source |
|---|---|---|---|
| Custom TCP | TCP | 8000 | webserverSG-id |
The inbound rules for the dbserverSG look as follows:
| Type | Protocol | Port | Source |
|---|---|---|---|
| MYSQL | TCP | 3306 | backendServerSG-id |
The outbound rules are still the default of all traffic allowed. We will edit the outbound rules of a security group later in the article.
Let's run the deploy command:
npx aws-cdk deploy
After deploying, the inbound rules of the backend server security group show
that it only allows traffic on port 8000 from requests made from instances
within the web server security group:

The security group of the database server shows that it only allows traffic on
port 3306 from requests made from instances within the backend server security
group:

Next, we are going to take a look at how we can edit the default outbound rules of a security group.
In order to edit the outbound rules of a security group in CDK, we have to set
the allowAllOutbound prop to false, when instantiating the SecurityGroup
class.
Let's create a security group and customize its outbound traffic rules.
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐ create a SG with custom Outbound rules const customOutboundSG = new ec2.SecurityGroup(this, 'custom-outbound-sg', { vpc, allowAllOutbound: false, description: 'a security group with custom outbound rules', }); customOutboundSG.addEgressRule( ec2.Peer.ipv4('10.0.0.0/16'), ec2.Port.tcp(3306), 'allow outgoing traffic on port 3306', ); customOutboundSG.addEgressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'allow outgoing traffic on port 80', ); } }
Let's go over the code snippet.
allowAllOutbound prop
to false. This is necessary if we are going to edit the outbound rules for
a security group, otherwise, our egress rules would just get ignored.addEgressRule method on an instance of the SecurityGroup
class.The outbound rules we added to the security group look as follows:
| Type | Protocol | Port | Destination |
|---|---|---|---|
| MYSQL | TCP | 3306 | 10.0.0.0/16 |
| HTTP | TCP | 80 | 0.0.0.0/0 |
Let's deploy the changes:
npx aws-cdk deploy
If we look at the VPC management console, we can see that the outbound rules have been applied to the security group:

Note that we haven't added any inbound rules to the security group, so it has none:

To delete the resources we have provisioned, issue the destroy command:
npx aws-cdk destroy
In order to import an existing security group into a CDK stack, we have to use the fromSecurityGroupId static method on the SecurityGroup class.
Let's look at an example of importing a security group in a CDK stack:
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ๐ import security group by ID const importedSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId( this, 'imported-security-group', 'YOUR-SG-ID', {allowAllOutbound: true, mutable: true}, ); console.log('security group id ๐', importedSecurityGroup.securityGroupId); } }
Let's go over the code snippet.
We imported a security group into our CDK stack by using the
fromSecurityGroupId static method on the SecurityGroup class
The fromSecurityGroupId method takes the following parameters:
scope - the scope the method is invoked in
id - the construct identifier (must be unique
in the scope)
securityGroupId - the id of the security group
securityGroupImportOptions - a configuration object for the imported
security group
The allowAllOutbound property is set to true by default and specifies that
the security group allows all outbound traffic. The fromSecurityGroupId
method assumes that the imported security group allows all outbound traffic,
so it doesn't modify any of the egress rules. If we wanted to modify the
outbound rules of the imported security group, we would have to set
allowAllOutbound to false.
The mutable property is also set to true by default. Setting the
property to true allows us to add rules to the imported security group. We
can only add inbound rules unless allowAllOutbound is set to false.
In order to add an inbound rule to an imported security group in CDK, we have to:
mutable property to true when importing the security group. The
mutable prop is set to true by default, so we can omit passing it
altogetherimport * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐ `mutable` is `true`, so we can add ingress rules importedSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'allow SSH access from anywhere', ); } }
We used the addIngressRule method to add the following inbound rule to the
imported security group:
| Type | Protocol | Port | Source |
|---|---|---|---|
| SSH | TCP | 22 | 0.0.0.0/0 |
If I run the npx aws-cdk deploy command with an existing security group ID, we
can see that the inbound rule gets applied:

Note that if we were to destroy the CDK stack, the inbound rule would get deleted and removed from the security group.
In order to add an outbound rule to an imported security group in CDK, we have to:
allowAllOutbound property to false and the mutable property to
trueconst importedSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId( this, 'imported-security-group', 'sg-0364cc5f9a979e9a6', {allowAllOutbound: false, mutable: true}, );
Let's look at an example, where we add an egress rule to an imported security group:
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐ `mutable` is `true`, so we can add ingress rules importedSecurityGroup.addEgressRule( ec2.Peer.ipv4('10.0.0.0/16'), ec2.Port.tcp(3306), 'allow outgoing traffic on port 3306', ); } }
We used the addEgressRule method on the imported security group to add the
following outbound rule:
| Type | Protocol | Port | Destination |
|---|---|---|---|
| MYSQL | TCP | 3306 | 10.0.0.0/16 |
If I deploy the new egress rule, we can see that the outbound rules of the imported security group get updated:

If we take a look at the resources the CloudFormation stack has provisioned, we can see the ingress and egress security group rules:

Deleting the stack would remove all of the ingress and egress rules we added to the imported security group.
To remove an inbound rule from a security group, we need to:
revoke-security-group-ingress command, passing in details that
identify the rule to be removedTo get the security group ID, open your AWS console, or if you've named the
security groups in that specific region uniquely, run the
describe-security-groups command:
aws ec2 describe-security-groups --query 'SecurityGroups[*].{sgName:GroupName,sgId:GroupId,vpcId:VpcId}'
The command returns a list of the security group names, IDs and VPC IDs:

Next, run the revoke-security-group-ingress command passing in the details
that identify the rule to be removed:
aws ec2 revoke-security-group-ingress --group-id sg-ABC123 --protocol tcp --port 80 --cidr 0.0.0.0/0

We removed an inbound rule that allows HTTP traffic on port 80 from anywhere.
All, use the -1 value for those parameters in your call to the revoke-security-group-ingress command.To verify that the rule has been removed, run the describe-security-groups
command:
aws ec2 describe-security-groups --group-ids sg-ABC123
--group-name parameter to identify the security group. Note that you can only use the --group-name parameter when the security group is in your default VPC in that region.If the security group you're trying to remove a rule from isn't in your default
VPC, you must use the --group-id
parameter.
If the rule is not found in the security group, the AWS CLI throws an error: "The specified rule does not exist in this security group".
If your inbound rule specifies a port range or you want to remove multiple
inbound rules, use the --ip-permissions parameter in the call to
revoke-security-group-ingress:
aws ec2 revoke-security-group-ingress --group-id sg-ABC123 --ip-permissions "[{\"IpProtocol\": \"tcp\", \"FromPort\": 80, \"ToPort\": 90, \"IpRanges\": [{\"CidrIp\": \"0.0.0.0/0\"}]}]"

We removed an inbound rule that allows TCP traffic from anywhere to a port range
of 80-90.
The syntax with the --ip-permission parameter is quite tricky, make sure you
escape the double quotes in the json input.
If you need to remove multiple inbound rules with a single command pass multiple
objects in the list of the --ip-permission parameter.
To remove a security group outbound rule with the AWS CLI, run the
revoke-security-group-egress command, passing in parameters that identify the
rule you're trying to remove.
aws ec2 revoke-security-group-egress --group-id sg-ABC123 --protocol icmp --port -1 --cidr 0.0.0.0/0

The command above removes an outbound rule that allows icmp traffic on all
ports to everywhere.
All ports by passing in the -1 value to the --port parameter.To verify that the rule has been removed, run the describe-security-groups
command:
aws ec2 describe-security-groups --group-ids sg-ABC123
If your outbound rule specifies a port range or you want to remove multiple
outbound rules, use the --ip-permissions parameter in the call to
revoke-security-group-egress:
aws ec2 revoke-security-group-egress --group-id sg-ABC123 --ip-permissions "[{\"IpProtocol\": \"tcp\", \"FromPort\": 80, \"ToPort\": 90, \"IpRanges\": [{\"CidrIp\": \"0.0.0.0/0\"}]}]"

In the command above, we removed an outbound rule that allows TCP traffic on
ports 80-90 to everywhere.
To remove multiple inbound rules in a single call to
revoke-security-group-egress, pass multiple objects in the list of the
--ip-permissions parameter.
You can learn more about the related topics by checking out the following tutorials: