Skip to content

Create security groups

Three chained firewalls so only the load balancer reaches the API and only the API reaches the database.

About 15 min · Verified 8 October 2026

0 of 4 steps done0%

A security group is a stateful firewall attached to a resource. The trick in this design is that each group only accepts traffic from the previous group, not from an IP range. The internet can reach the load balancer; only the load balancer can reach the API; only the API can reach the database.

text
Internet ──:80──► shortlink-alb-sg ──:3000──► shortlink-api-sg ──:5432──► shortlink-db-sg

Create them in this order, because each rule refers to the group before it, and the group has to exist before you can pick it.

Create the three groups#

Open VPCSecurity groupsCreate security group (or EC2Security Groups).

FieldValue
Security group nameshortlink-alb-sg
DescriptionPublic HTTP to the load balancer
VPCshortlink-vpc (not the default VPC)

Inbound rules → Add rule:

TypeProtocolPortSource
HTTPTCP80Anywhere-IPv4 (0.0.0.0/0)

Leave Outbound rules at the default (all traffic). Choose Create security group.

Create another group in shortlink-vpc:

FieldValue
Security group nameshortlink-api-sg
DescriptionAPI server, reachable only from the load balancer
VPCshortlink-vpc

Inbound rules → Add rule:

TypeProtocolPortSource
Custom TCPTCP3000Custom → start typing shortlink-alb-sg and pick the group

Leave outbound at the default. The instance needs outbound access for apt, npm, Systems Manager and the database.

FieldValue
Security group nameshortlink-db-sg
DescriptionPostgreSQL, reachable only from the API
VPCshortlink-vpc

Inbound rules → Add rule:

TypeProtocolPortSource
PostgreSQLTCP5432Custom → shortlink-api-sg

Outbound can stay at the default.

Verify the rules#

Your computerShow every inbound rule
aws ec2 describe-security-groups --region ap-south-1 \
  --filters Name=group-name,Values='shortlink-*' \
  --query 'SecurityGroups[].{name:GroupName,id:GroupId,inbound:IpPermissions[].{port:FromPort,cidr:IpRanges[0].CidrIp,fromGroup:UserIdGroupPairs[0].GroupId}}' \
  --output json

Check three things:

  • shortlink-alb-sg: port 80, source CIDR 0.0.0.0/0.
  • shortlink-api-sg: port 3000, fromGroup is the ID of shortlink-alb-sg, and there is no CIDR.
  • shortlink-db-sg: port 5432, fromGroup is the ID of shortlink-api-sg.
Why is a rule showing an ID like sg-0abc… instead of a name?

Rules that refer to another group always store its ID. The console shows the ID with the name next to it. Compare the IDs with the output of the command above.

Next: create the database.

Found a mistake? Edit this page on GitHub.