EC2 instances don’t need to talk to the internet unless you explicitly tell them to.
Let’s see an EC2 instance behind a public IP address, running a web server. We’ll start with a wide-open security group and then tighten it down.
Imagine this launch-template.json for our instance:
{
"ImageId": "ami-0abcdef1234567890",
"InstanceType": "t3.micro",
"NetworkInterfaces": [
{
"DeviceIndex": 0,
"SubnetId": "subnet-0123456789abcdef0",
"Groups": ["sg-00000000000000000"],
"AssociatePublicIpAddress": true
}
],
"UserData": "#!/bin/bash\nsudo apt-get update -y\nsudo apt-get install -y apache2\nsudo systemctl start apache2"
}
And here’s our initial, overly permissive security group sg-00000000000000000 in AWS CLI format:
aws ec2 create-security-group --group-name "webserver-open" --description "Allows all inbound traffic" --vpc-id vpc-0123456789abcdef0
Then we add rules:
aws ec2 authorize-security-group-ingress --group-id sg-00000000000000000 --protocol all --port all --cidr 0.0.0.0/0
Now, if we launch an instance using the launch-template.json, we can curl its public IP from anywhere. The web server is accessible globally. This is convenient for initial setup but a massive security risk.
The problem this solves is the inherent insecurity of default "allow all" network configurations. By default, cloud providers often err on the side of making things work easily, which means opening up network paths that aren’t strictly necessary. Least privilege means an entity (user, service, or instance) should only have the permissions and access it needs to perform its function, and no more. For network access, this translates to only allowing specific protocols, ports, and source IP addresses that are essential for the instance’s operation.
Internally, security groups act as a virtual firewall for your EC2 instances. They control inbound and outbound traffic at the instance level. When traffic hits your instance, the security group rules are evaluated. If an inbound rule matches the traffic (protocol, port, and source IP), it’s allowed. If no rule matches, it’s denied by default. For outbound traffic, the default is to allow all, but you can restrict it too.
The key levers you control are:
- Protocol: TCP, UDP, ICMP, or
all. - Port Range: Specific port numbers (e.g., 80, 443) or
all. - Source/Destination: IP addresses or CIDR blocks, or other security groups (for inter-instance communication within a VPC).
Let’s tighten our sg-00000000000000000 for a web server. We only need to allow inbound HTTP (port 80) and HTTPS (port 443) from the internet.
First, remove the overly permissive rule:
aws ec2 revoke-security-group-ingress --group-id sg-00000000000000000 --protocol all --port all --cidr 0.0.0.0/0
Now, add specific rules:
aws ec2 authorize-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 443 --cidr 0.0.0.0/0
This configuration now allows only HTTP and HTTPS traffic from anywhere on the internet to reach your web server. This is a significant improvement.
For enhanced security, you might want to restrict access to only specific IP addresses or ranges known to be legitimate administrators or monitoring services. For example, if your office has a static IP 203.0.113.10/32:
aws ec2 revoke-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 revoke-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 80 --cidr 203.0.113.10/32
aws ec2 authorize-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 443 --cidr 203.0.113.10/32
You also need to consider outbound rules. By default, your EC2 instance can initiate connections to anywhere. If your web server only needs to connect to an external API on port 12345, you should restrict outbound traffic to only that destination and port.
aws ec2 authorize-security-group-egress --group-id sg-00000000000000000 --protocol tcp --port 12345 --cidr 192.0.2.10/32
The most surprising thing about security groups is their stateful nature. If you allow an inbound connection on port 80, the return traffic for that established connection is automatically allowed outbound, without you needing to define a specific outbound rule for it. This simplifies rule management significantly because you don’t have to track the ephemeral ports used for outbound responses.
Finally, remember that security groups are associated with network interfaces, not directly with instances. An instance can have multiple network interfaces, each potentially with its own security groups. You can also reference other security groups in your rules, allowing instances within one security group to communicate with instances in another, which is a powerful mechanism for micro-segmentation within your VPC.
The next step after perfectly defining inbound and outbound rules is understanding Network ACLs and how they interact with security groups.