Harden EC2 with Security Groups
Learn to harden EC2 instances using security groups: restrict inbound traffic, apply least-privilege rules, and reduce your attack surface in this practical Cloud security essentials lesson.
Focus: harden ec2 with security groups
Imagine waking up to a cloud bill that's 10x your normal spend — or worse, a notification that your EC2 instance has been compromised and is now mining cryptocurrency for someone else. Both scenarios happen every day, and the root cause is often the same: security groups configured with wide-open rules like 0.0.0.0/0 on SSH or RDP. The good news? You can dramatically reduce your attack surface in minutes by hardening your EC2 instances with security groups. In this lesson, you'll learn the mental model behind security groups, how to configure them step by step, and how to avoid the most common pitfalls — all hands-on, with real AWS CLI commands you can run today.
The problem this lesson solves
Every EC2 instance you launch is a potential entry point for attackers. By default, a security group allows all outbound traffic and denies all inbound traffic — but many developers, in a rush to get things working, open up ports like 22 (SSH), 3389 (RDP), or 3306 (MySQL) to 0.0.0.0/0. That means anyone on the internet can attempt to connect to your instance. Brute-force attacks, vulnerability scanning, and data exfiltration follow quickly if you don't close those doors.
The problem isn't just about attackers — it's about operational discipline. If you don't understand how security groups work, you'll either lock yourself out (denying your own IP) or expose your data (opening too many ports). Both outcomes are costly. Hardening EC2 with security groups means deliberately restricting inbound traffic to only what's necessary and from only who's allowed.
Core concept / mental model
Think of a security group as a stateful firewall at the instance level. Every EC2 instance has one or more security groups attached, and all inbound and outbound traffic is filtered according to the rules in those groups.
Here's the mental model that makes everything click:
- Inbound rules control what traffic can reach your instance. If you don't create an inbound rule, nothing gets in.
- Outbound rules control what traffic can leave your instance. By default, all outbound is allowed — and that's usually fine, but you can restrict it too.
- Stateful nature: If you allow inbound traffic from a specific IP and port, the response traffic is automatically allowed, regardless of outbound rules. This is a key difference from network ACLs (stateless).
- Least privilege: Only open the ports you need, and only to the IP ranges that need access. For example, instead of allowing SSH from
0.0.0.0/0, allow it only from your office's public IP or a bastion host. - Security groups are not hierarchical: A security group can reference another security group as a source, which is a powerful way to control traffic between tiers without hard-coding IPs.
Pro tip: Security groups are scoped to a VPC and a region. You can't attach a security group from one VPC to an instance in another. Plan your rules per environment (dev, staging, prod) to avoid mixing settings.
How it works step by step
Here's the logical sequence you'll follow every time you harden an EC2 instance:
- Identify the instance's purpose — Is it a web server, a database, an application server? Each role has a different set of required ports.
- List required inbound ports — For a web server, that's typically 80 and 443 from the internet. For a database, it's 3306 or 5432, but only from your application servers — never from the internet.
- Define allowed source IPs — For administrative access (SSH/RDP), use your own public IP (or a VPN/CIDR). For service-to-service communication, use security group references or specific CIDRs.
- Create the security group — Either in the AWS Management Console, via CLI, or using Infrastructure as Code (IaC) like Terraform.
- Attach the security group — When launching an instance or by modifying existing instances (you can attach/detach security groups to running instances).
- Test your rules — Use
nc(netcat) ortelnetto verify what's open and what's not. Tools like AWS Console's "Diagnostics" tab or theaws ec2 describe-security-groupsCLI can help. - Review and iterate — Security groups are dynamic. As your app evolves, so will your rules. Schedule periodic reviews to remove stale or overly permissive rules.
Cause → effect: If you open port 3306 to
0.0.0.0/0, your database is visible to the internet — attackers will scan for it. If you restrict it to your app's security group, only instances with that group can reach it.
Hands-on walkthrough
Let's put this into practice. We'll create a security group that allows SSH only from your IP and HTTP/HTTPS from anywhere. We'll also clean up after ourselves.
Prerequisites
- AWS CLI installed and configured (
aws configure) jqfor JSON parsing (optional)- An existing VPC (or use the default)
Step 1: Get your VPC ID and your IP
VPC_ID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query 'Vpcs[0].VpcId' --output text)
MY_IP=$(curl -s https://checkip.amazonaws.com)
echo "VPC: $VPC_ID"
echo "Your IP: $MY_IP"
Step 2: Create the security group
SG_ID=$(aws ec2 create-security-group --group-name WebServerSG --description "Web server security group" --vpc-id "$VPC_ID" --output text)
echo "Security Group ID: $SG_ID"
Step 3: Add inbound rules
# Allow SSH from your IP only (least privilege)
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" \
--protocol tcp \
--port 22 \
--cidr "${MY_IP}/32"
# Allow HTTP and HTTPS from anywhere
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
echo "Inbound rules added."
Expected output: The commands return the GroupId of the updated security group. No output for authorize-security-group-ingress means success.
Step 4: Verify your rules
aws ec2 describe-security-groups --group-ids "$SG_ID" --query 'SecurityGroups[0].IpPermissions' --output json | jq .
You should see three rules: SSH from your IP, and HTTP/HTTPS from 0.0.0.0/0.
Step 5: Launch a test instance (optional)
# Get a default subnet ID
SUBNET_ID=$(aws ec2 describe-subnets --filters Name=vpc-id,Values="$VPC_ID" Name=default-for-az,Values=true --query 'Subnets[0].SubnetId' --output text)
# Launch a t2.nano instance (free tier eligible)
INSTANCE_ID=$(aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t2.nano \
--security-group-ids "$SG_ID" \
--subnet-id "$SUBNET_ID" \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=TestHarden}]' \
--query 'Instances[0].InstanceId' --output text)
echo "Instance ID: $INSTANCE_ID"
Step 6: Test connectivity (from a different IP)
If you have a second machine with a different IP, try to SSH to your instance. You should get a timeout because only your original IP is allowed.
Step 7: Clean Up
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"
aws ec2 delete-security-group --group-id "$SG_ID"
Pro tip: Use the
--dry-runflag onaws ec2 authorize-security-group-ingressto validate your rules without actually applying them.
Compare options / when to choose what
You have several ways to control network access to your EC2 instances. Here's a comparison:
| Feature | Security Groups | Network ACLs | AWS WAF |
|---|---|---|---|
| Scope | Instance-level | Subnet-level | Application-level (HTTP) |
| State | Stateful | Stateless | Stateless |
| Rules | Allow only | Allow/Deny | Custom rules (IP, rate, headers) |
| Best for | Instance protection | Subnet-wide protection | Web app filtering |
| Cost | Free | Free | Pay per rule/request |
| Typical use | Restrict SSH, open web ports | Add a deny-all or block known bad CIDRs | Block SQL injection, DDoS protection |
When to choose what:
- Use security groups by default — they're free, stateful, and easy to manage.
- Use network ACLs when you need a subnet-wide blocking layer (e.g., deny a problematic IP range) or when you need stateless filtering for compliance.
- Use AWS WAF for web-facing apps that need advanced filtering at the application layer (e.g., blocking specific user agents, rate limiting).
Troubleshooting & edge cases
Even careful engineers hit issues. Here are common scenarios and fixes:
1. "I can't SSH to my instance even after adding my IP"
Cause: Your public IP may have changed (DHCP) — the rule was set for an old IP. Or you used 0.0.0.0/0 and your ISP's IP is in a range you didn't think of.
Fix: Re-check your current IP with curl https://checkip.amazonaws.com and update the rule. Also verify that the instance has a public IP and that your key pair is correct.
2. "My web server works internally but is inaccessible from the internet"
Cause: The security group allows port 80, but the instance's OS firewall (e.g., iptables or ufw) is blocking it.
Fix: Check your OS firewall settings. Security groups only filter traffic before it reaches the instance — the OS can still drop it.
3. "I accidentally deleted the default security group and now I can't connect"
Cause: You may have removed the only security group that allowed SSH.
Fix: Use the EC2 Instance Connect feature (if enabled) or the AWS console's Connect from your office IP. Alternatively, create a new security group and attach it via the console or CLI while the instance is running.
4. "Too many rules, hard to manage"
Fix: Use security group references instead of CIDRs. For example, allow the app tier's security group (e.g., sg-123456) to access the database port. This way, if app instances change IPs, no rule updates are needed.
5. "Port scanning shows my SSH open to the world"
Cause: You used 0.0.0.0/0 for SSH instead of your IP.
Fix: Immediately restrict SSH to your IP (or a bastion host). Use the AWS CLI to update the rule:
aws ec2 revoke-security-group-ingress --group-id sg-xxxx --protocol tcp --port 22 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-xxxx --protocol tcp --port 22 --cidr YOUR_IP/32
What you learned & what's next
In this lesson, you learned why hardening EC2 with security groups is critical: you now understand the mental model of stateful, allow-list-based rules, and you've practiced creating, attaching, and testing security groups. You can apply least privilege by restricting inbound traffic to specific ports and IPs, and you know how to choose between security groups, network ACLs, and WAF.
Next lesson in the Cloud security essentials track: we'll dive into IAM roles for EC2 — how to avoid hard-coded credentials on instances and instead grant temporary, scoped permissions. This pairs perfectly with what you've done here, because both controls work together to shrink your attack surface.
Key takeaway: Security groups are your first line of defense. Never open a port to the world unless you absolutely must. Start with deny-all, then allow only what's needed. Your future self — and your cloud bill — will thank you.
Practice recap
Try this mini-exercise: launch a new EC2 instance, then create a security group that allows your IP for SSH only, attache it, and test that you can SSH from your IP but not from a different one. Then remove the rule and verify the timeout. This hands-on drill will reinforce the lesson.
Common mistakes
- Opening SSH (port 22) to 0.0.0.0/0 – this invites brute-force attacks. Always restrict to your IP or use a bastion host.
- Using security groups only on launch – forgetting that you can't edit rules on running instances without first creating a new group and attaching it.
- Assuming security groups are stateless like network ACLs – they're stateful, which means response traffic is automatically allowed, so don't add unnecessary outbound rules.
- Deleting a security group that's attached to an instance – this can break connectivity or cause downtime. Always detach before deleting.
Variations
- Use AWS Systems Manager Session Manager for SSH-like access without opening port 22 at all – great for Windows instances.
- Use Terraform or CloudFormation to declare security groups as code, making them repeatable and reviewable.
- Implement a bastion host (jump box) with a hardened security group to centralize administrative access.
Real-world use cases
- Web application servers with security groups that allow 80/443 from the internet and 22 only from your office's public IP.
- Database servers (MySQL/PostgreSQL) configured to accept traffic only from application server security groups, not from the internet.
- Monitoring agents (e.g., Prometheus) that expose metrics on a port you restrict to a separate monitoring VPC or security group.
Key takeaways
- Security groups are stateful instance-level firewalls that allow you to use least-privilege inbound rules.
- Always restrict SSH/RDP to your IP or a bastion host — never 0.0.0.0/0 for administrative ports.
- Left to default, all inbound is denied — start with deny-all and add only what you need.
- Use security group references instead of CIDRs for inter-tier communication to reduce rule churn.
- Test your rules with netcat or telnet after configuration; your OS firewall can still block traffic.
- Clean up unused security groups to avoid audit findings and reduce your attack surface.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.