← Back to Tutorials
Tutorial 04 Intermediate

EC2 Security Hardening: Protecting Your Compute Infrastructure

EC2 instances are often the crown jewels of AWS environmentsβ€”they process sensitive data, connect to critical databases, and can serve as launch points for attackers. Learn how to implement defense-in-depth security for your EC2 fleet.

45 min implementation
18 min read
Compute Security & Hardening

Why EC2 Security Failures Are Catastrophic

A single compromised EC2 instance can lead to complete infrastructure takeover. Attackers use compromised instances to access S3 buckets, launch additional resources, steal credentials, and deploy ransomware across your entire environment. The vast majority of successful EC2 attacks exploit known vulnerabilities in unpatched software.

The Six Most Dangerous EC2 Attack Vectors

1

Unpatched Software Vulnerabilities

The vast majority of successful EC2 attacks exploit known vulnerabilities in unpatched operating systems, web servers, and applications with public CVEs.

2

Overprivileged IAM Roles

Instance metadata attacks steal IAM credentials from compromised instances. Overprivileged roles allow attackers to access additional AWS resources and escalate privileges.

3

Weak Access Controls

SSH key compromises, weak passwords, default credentials, shared keys, and overly permissive security groups create multiple attack pathways.

4

Network Exposure

Public IP addresses with open ports expose instances to internet-wide attacks. Unrestricted security groups and missing network segmentation enable lateral movement.

5

Unencrypted Data at Rest

Unencrypted EBS volumes and snapshots expose sensitive data. Missing encryption, shared snapshots, and inadequate backup security provide data access to attackers.

6

Insufficient Monitoring

Blind spot exploitation allows attacks to persist undetected. Missing logging, no intrusion detection, and inadequate alerting enable long-term compromise.

The Four Critical Business Risks

  • Ransomware & Data Encryption: Direct file encryption, network propagation through shared credentials, cloud resource hijacking, and backup destruction
  • Cryptocurrency Mining: Massive unexpected AWS bills ($50,000+ monthly), performance degradation, botnet participation
  • Data Exfiltration: Database credentials in config files, S3 access through overprivileged roles, API keys in environment variables
  • Compliance Violations: PCI DSS Requirement 6.1 (patching), HIPAA Β§164.308(a)(5) (endpoint security), SOC 2 CC6.8 (vulnerability management)
πŸ’‘
Defense Layers: Effective EC2 security requires multiple layers: network perimeter (security groups, NACLs), instance hardening (patching, configuration), access controls (IAM, SSH), data protection (encryption), and monitoring (GuardDuty, CloudWatch).
1

Implement Security Groups Defense-in-Depth

~12 minutes

Security groups are your first line of defense against network-based attacks. Properly configured security groups can prevent the majority of common EC2 compromises by blocking unauthorized access at the network layer.

Prerequisites

  • List of all EC2 instances and their required network access
  • Understanding of application architecture and data flows
  • Administrative access to EC2 and VPC services

Console Steps

1.1 Audit Current Security Groups

  • Navigate to EC2 Console β†’ Security Groups
  • Review all security groups for overly permissive rules
  • Look for rules allowing 0.0.0.0/0 access on sensitive ports
  • Identify unused or default security groups
Audit Security Groups via CLI
# Find security groups with overly permissive rules
aws ec2 describe-security-groups \
    --query 'SecurityGroups[?IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' \
    --output table

# Find security groups allowing SSH from anywhere
aws ec2 describe-security-groups \
    --query 'SecurityGroups[?IpPermissions[?FromPort==`22` && IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' \
    --output table

# Find security groups allowing RDP from anywhere
aws ec2 describe-security-groups \
    --query 'SecurityGroups[?IpPermissions[?FromPort==`3389` && IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' \
    --output table

1.2 Create Layered Security Groups

Create specific security groups for each application tier:

  • web-tier-sg - Web servers (ports 80, 443 only)
  • app-tier-sg - Application servers (specific app ports)
  • db-tier-sg - Database servers (database ports only)
  • admin-access-sg - Administrative access (SSH/RDP from specific IPs)
Create Layered Security Groups
# Create web tier security group
aws ec2 create-security-group \
    --group-name web-tier-sg \
    --description "Web tier security group - HTTP/HTTPS only" \
    --vpc-id vpc-12345678

# Allow HTTP and HTTPS from anywhere (for public web servers)
aws ec2 authorize-security-group-ingress \
    --group-id sg-web-tier-id \
    --protocol tcp \
    --port 80 \
    --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress \
    --group-id sg-web-tier-id \
    --protocol tcp \
    --port 443 \
    --cidr 0.0.0.0/0

# Create admin access security group (restrict to office IPs)
aws ec2 create-security-group \
    --group-name admin-access-sg \
    --description "Administrative access - SSH/RDP from office only" \
    --vpc-id vpc-12345678

# Allow SSH from office IP range only
aws ec2 authorize-security-group-ingress \
    --group-id sg-admin-id \
    --protocol tcp \
    --port 22 \
    --cidr 203.0.113.0/24
⚠️
Critical: Never allow SSH (port 22) or RDP (port 3389) from 0.0.0.0/0. This exposes your instances to brute force attacks from across the internet. Use VPN, bastion hosts, or AWS Systems Manager Session Manager for remote access.

1.3 Configure Security Group Chaining

Use security group references instead of IP addresses for internal communication:

Security Group Chaining
# Allow app tier to access database tier (PostgreSQL)
aws ec2 authorize-security-group-ingress \
    --group-id sg-db-tier-id \
    --protocol tcp \
    --port 5432 \
    --source-group sg-app-tier-id

# Allow web tier to access app tier
aws ec2 authorize-security-group-ingress \
    --group-id sg-app-tier-id \
    --protocol tcp \
    --port 8080 \
    --source-group sg-web-tier-id
βœ…
Network Secured! Your EC2 instances now have layered network protection with least-privilege access controls, blocking most automated attack attempts.
2

Enable IMDSv2 & Automated Patching

~10 minutes

Instance Metadata Service v2 (IMDSv2) prevents Server-Side Request Forgery (SSRF) attacks that steal IAM credentials. Automated patching through Systems Manager prevents exploitation of known vulnerabilities.

Enable IMDSv2

2.1 Enable IMDSv2 for New Instances

  • In EC2 Launch Template or Launch Instance wizard
  • Expand Advanced details section
  • Set Metadata version to "V2 only (token required)"
  • Set Metadata response hop limit to 1

2.2 Update Existing Instances

  • Select existing EC2 instances in console
  • Actions β†’ Instance settings β†’ Modify instance metadata options
  • Set Metadata version to "V2 only"
  • Set Metadata response hop limit to 1
  • Click Save
Enable IMDSv2 via CLI
# Enable IMDSv2 for specific instance
aws ec2 modify-instance-metadata-options \
    --instance-id i-1234567890abcdef0 \
    --http-tokens required \
    --http-put-response-hop-limit 1

# Bulk update all running instances
for instance in $(aws ec2 describe-instances \
    --query 'Reservations[*].Instances[?State.Name==`running`].InstanceId' \
    --output text); do
    echo "Updating instance: $instance"
    aws ec2 modify-instance-metadata-options \
        --instance-id $instance \
        --http-tokens required \
        --http-put-response-hop-limit 1
done

# Test IMDSv2 from within instance
# Get token (required for IMDSv2)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

# Use token to access metadata
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/instance-id

Configure Automated Patching

2.3 Ensure SSM Agent is Installed

Modern Amazon Linux 2/2023 and Windows AMIs include SSM Agent. For older instances:

Install SSM Agent
# Install SSM Agent on Amazon Linux 2
sudo yum install -y amazon-ssm-agent
sudo systemctl enable amazon-ssm-agent
sudo systemctl start amazon-ssm-agent

# Install SSM Agent on Ubuntu
sudo snap install amazon-ssm-agent --classic
sudo systemctl enable snap.amazon-ssm-agent.amazon-ssm-agent.service
sudo systemctl start snap.amazon-ssm-agent.amazon-ssm-agent.service

# Create IAM role for SSM
aws iam create-role \
    --role-name EC2-SSM-Role \
    --assume-role-policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "ec2.amazonaws.com"},
            "Action": "sts:AssumeRole"
        }]
    }'

# Attach managed policy
aws iam attach-role-policy \
    --role-name EC2-SSM-Role \
    --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

2.4 Create Maintenance Window for Patching

  • Navigate to Systems Manager β†’ Maintenance Windows
  • Click Create maintenance window
  • Schedule: Weekly during low usage periods (e.g., Sundays 2 AM UTC)
  • Duration: 2-4 hours depending on environment size
  • Use instance tags to group similar systems
Create Maintenance Window
# Create maintenance window
aws ssm create-maintenance-window \
    --name "WeeklyPatchingWindow" \
    --description "Weekly patching for production instances" \
    --duration 4 \
    --cutoff 1 \
    --schedule "cron(0 2 ? * SUN *)" \
    --schedule-timezone "UTC" \
    --allow-unassociated-targets

# Tag instances for patch groups
aws ec2 create-tags \
    --resources i-1234567890abcdef0 \
    --tags Key=PatchGroup,Value=Production

# Check patch compliance
aws ssm describe-instance-patch-states \
    --instance-ids i-1234567890abcdef0
⚠️
Testing Required: Always test patches in a staging environment before applying to production. Some patches may cause application compatibility issues or require restarts.
βœ…
SSRF & Vulnerabilities Protected! Your EC2 instances are now protected against credential theft via SSRF attacks and receive automatic security patches.
3

Configure Encryption & Network Segmentation

~10 minutes

EBS encryption protects data at rest, while proper network segmentation contains security breaches and prevents lateral movement by attackers.

Enable EBS Encryption

3.1 Enable EBS Encryption by Default

  • Navigate to EC2 β†’ Account attributes β†’ EBS encryption
  • Click Manage and enable "Always encrypt new EBS volumes"
  • Select default encryption key (AWS managed or customer managed)
  • Apply to all regions where you operate
Enable EBS Encryption by Default
# Enable EBS encryption by default
aws ec2 enable-ebs-encryption-by-default

# Optionally set a custom KMS key
aws ec2 modify-ebs-default-kms-key-id \
    --kms-key-id arn:aws:kms:REGION:ACCOUNT:key/KEY-ID

# Check encryption status
aws ec2 get-ebs-encryption-by-default

# Find unencrypted volumes
aws ec2 describe-volumes \
    --query 'Volumes[?Encrypted==`false`].[VolumeId,State,Attachments[0].InstanceId]' \
    --output table

3.2 Encrypt Existing Volumes

For existing unencrypted volumes:

  • Create snapshots of unencrypted volumes
  • Copy snapshots with encryption enabled
  • Create new encrypted volumes from encrypted snapshots
  • Replace unencrypted volumes during maintenance windows

Implement Network Segmentation

3.3 Create Isolated Subnets

  • Create public subnets only for load balancers and NAT gateways
  • Place application servers in private subnets
  • Isolate databases in dedicated private subnets
  • Use separate subnets per availability zone for high availability
Create Isolated Subnets
# Create private subnet for application tier
aws ec2 create-subnet \
    --vpc-id vpc-12345678 \
    --cidr-block 10.0.1.0/24 \
    --availability-zone us-east-1a \
    --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=app-private-1a}]'

# Create private subnet for database tier
aws ec2 create-subnet \
    --vpc-id vpc-12345678 \
    --cidr-block 10.0.2.0/24 \
    --availability-zone us-east-1a \
    --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=db-private-1a}]'

3.4 Configure Network ACLs

Add Network ACLs as an additional layer beyond security groups:

Configure Network ACLs
# Create restrictive NACL for database subnet
aws ec2 create-network-acl \
    --vpc-id vpc-12345678 \
    --tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=db-tier-nacl}]'

# Allow inbound database traffic only from app tier
aws ec2 create-network-acl-entry \
    --network-acl-id acl-db123456 \
    --rule-number 100 \
    --protocol tcp \
    --rule-action allow \
    --ingress \
    --port-range From=5432,To=5432 \
    --cidr-block 10.0.1.0/24

# Deny all other inbound traffic
aws ec2 create-network-acl-entry \
    --network-acl-id acl-db123456 \
    --rule-number 200 \
    --protocol -1 \
    --rule-action deny \
    --ingress \
    --cidr-block 0.0.0.0/0
βœ…
Data Protected & Network Hardened! Your EBS volumes are encrypted and your network architecture limits blast radius and prevents lateral movement.
4

Enable Monitoring & Intrusion Detection

~13 minutes

Comprehensive monitoring enables rapid detection of security incidents. GuardDuty uses machine learning to detect malicious activity, while Amazon Inspector identifies vulnerabilities. Least-privilege IAM roles limit the blast radius of compromises.

Enable GuardDuty

4.1 Enable GuardDuty

  • Navigate to GuardDuty service in AWS Console
  • Click Get started and Enable GuardDuty
  • GuardDuty begins analyzing VPC Flow Logs, DNS logs, and CloudTrail events
  • Initial setup takes 15-30 minutes to establish baseline behavior
Enable GuardDuty and Configure Alerts
# Enable GuardDuty
aws guardduty create-detector \
    --enable \
    --finding-publishing-frequency FIFTEEN_MINUTES

# Create SNS topic for alerts
aws sns create-topic --name guardduty-alerts

# Subscribe email to topic
aws sns subscribe \
    --topic-arn arn:aws:sns:REGION:ACCOUNT:guardduty-alerts \
    --protocol email \
    --notification-endpoint EMAIL ADDRESS

# Create EventBridge rule for high-severity findings
aws events put-rule \
    --name guardduty-high-severity \
    --event-pattern '{
        "source": ["aws.guardduty"],
        "detail-type": ["GuardDuty Finding"],
        "detail": {"severity": [7, 8, 9]}
    }'

Enable Amazon Inspector

4.2 Enable Amazon Inspector

  • Navigate to Amazon Inspector service
  • Click Get started and enable Inspector
  • Choose EC2 instances and ECR repositories for scanning
  • Inspector automatically scans when new instances launch or packages update
Enable Amazon Inspector
# Enable Inspector for EC2 and ECR
aws inspector2 enable --resource-types EC2 ECR

# List high and critical findings
aws inspector2 list-findings \
    --filter-criteria '{
        "severity": [
            {"comparison": "EQUALS", "value": "HIGH"},
            {"comparison": "EQUALS", "value": "CRITICAL"}
        ]
    }'

Configure CloudWatch Monitoring

4.3 Enable Detailed Monitoring

  • Select EC2 instances in console
  • Actions β†’ Monitor and troubleshoot β†’ Manage detailed monitoring
  • Enable detailed monitoring for 1-minute metrics
  • Set up alarms for CPU, network, and disk anomalies
CloudWatch Monitoring and Alarms
# Enable detailed monitoring
aws ec2 monitor-instances --instance-ids i-1234567890abcdef0

# Create alarm for high CPU (potential cryptomining)
aws cloudwatch put-metric-alarm \
    --alarm-name "EC2-High-CPU-Alert" \
    --alarm-description "Alert when CPU exceeds 80%" \
    --metric-name CPUUtilization \
    --namespace AWS/EC2 \
    --statistic Average \
    --period 300 \
    --threshold 80 \
    --comparison-operator GreaterThanThreshold \
    --evaluation-periods 2 \
    --alarm-actions arn:aws:sns:REGION:ACCOUNT:security-alerts \
    --dimensions Name=InstanceId,Value=i-1234567890abcdef0

# Create alarm for high network out (potential data exfiltration)
aws cloudwatch put-metric-alarm \
    --alarm-name "EC2-High-Network-Out" \
    --alarm-description "Alert on high outbound traffic" \
    --metric-name NetworkOut \
    --namespace AWS/EC2 \
    --statistic Average \
    --period 300 \
    --threshold 1000000000 \
    --comparison-operator GreaterThanThreshold \
    --evaluation-periods 1 \
    --alarm-actions arn:aws:sns:REGION:ACCOUNT:security-alerts

Implement Least-Privilege IAM Roles

4.4 Create Application-Specific IAM Roles

Create separate roles for different application functions with minimum permissions:

Least-Privilege IAM Role Example
# Create role for web application
aws iam create-role \
    --role-name WebApp-Role \
    --assume-role-policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "ec2.amazonaws.com"},
            "Action": "sts:AssumeRole"
        }]
    }'

# Create custom policy with specific S3 bucket access only
aws iam create-policy \
    --policy-name WebApp-S3-Access \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Action": ["s3:GetObject", "s3:PutObject"],
            "Resource": "arn:aws:s3:::your-app-bucket/*"
        }]
    }'

# Attach policy to role
aws iam attach-role-policy \
    --role-name WebApp-Role \
    --policy-arn arn:aws:iam::ACCOUNT:policy/WebApp-S3-Access
⚠️
Never Use: Avoid attaching "AdministratorAccess" or policies with "*" permissions to EC2 instance roles. This gives attackers full account access if the instance is compromised.
βœ…
Visibility & Detection Complete! Your EC2 instances now have comprehensive monitoring, intrusion detection, vulnerability scanning, and least-privilege access controls.

Validate Your Configuration

Complete these checks to ensure your EC2 security hardening is properly implemented:

Validation Script

ec2-security-validation.sh
#!/bin/bash
# EC2 Security Configuration Validation Script

echo "=== EC2 Security Validation ==="
echo ""

# Check security groups for open SSH
echo "Checking for permissive security groups..."
OPEN_SSH=$(aws ec2 describe-security-groups \
    --query 'SecurityGroups[?IpPermissions[?FromPort==`22` && IpRanges[?CidrIp==`0.0.0.0/0`]]].[GroupId,GroupName]' \
    --output text)
if [ -n "$OPEN_SSH" ]; then
    echo "βœ— WARNING: Security groups allow SSH from anywhere:"
    echo "$OPEN_SSH"
else
    echo "βœ“ No security groups allow SSH from anywhere"
fi
echo ""

# Check IMDSv2 enforcement
echo "Checking IMDSv2 enforcement..."
IMDSV1_INSTANCES=$(aws ec2 describe-instances \
    --query 'Reservations[*].Instances[?MetadataOptions.HttpTokens==`optional`].[InstanceId]' \
    --output text)
if [ -n "$IMDSV1_INSTANCES" ]; then
    echo "βœ— WARNING: Instances still allow IMDSv1:"
    echo "$IMDSV1_INSTANCES"
else
    echo "βœ“ All instances enforce IMDSv2"
fi
echo ""

# Check EBS encryption
echo "Checking EBS encryption..."
UNENCRYPTED=$(aws ec2 describe-volumes \
    --query 'Volumes[?Encrypted==`false`].[VolumeId]' \
    --output text)
if [ -n "$UNENCRYPTED" ]; then
    echo "βœ— WARNING: Unencrypted EBS volumes found:"
    echo "$UNENCRYPTED"
else
    echo "βœ“ All EBS volumes are encrypted"
fi
echo ""

# Check SSM agent connectivity
echo "Checking SSM agent connectivity..."
SSM_OFFLINE=$(aws ssm describe-instance-information \
    --query 'InstanceInformationList[?PingStatus!=`Online`].[InstanceId,PingStatus]' \
    --output text)
if [ -n "$SSM_OFFLINE" ]; then
    echo "βœ— Instances with SSM connectivity issues:"
    echo "$SSM_OFFLINE"
else
    echo "βœ“ All registered instances have healthy SSM connectivity"
fi
echo ""

# Check GuardDuty
echo "Checking GuardDuty..."
DETECTORS=$(aws guardduty list-detectors --query 'DetectorIds' --output text)
if [ -n "$DETECTORS" ]; then
    echo "βœ“ GuardDuty is enabled"
else
    echo "βœ— GuardDuty is not enabled"
fi
echo ""

echo "EC2 security validation complete!"

Common Mistakes to Avoid

βœ—

Allowing SSH/RDP from 0.0.0.0/0. Use bastion hosts, VPN, or Systems Manager Session Manager instead of exposing management ports to the internet.

βœ—

Using IMDSv1 (optional tokens). Always require IMDSv2 tokens to prevent SSRF attacks from stealing IAM credentials.

βœ—

Attaching AdministratorAccess to EC2 roles. Use least-privilege policies specific to each application's actual needs.

βœ—

Storing credentials in user data or config files. Use IAM roles, Secrets Manager, or Parameter Store instead of hardcoded credentials.

βœ—

Skipping patch testing. Always test patches in staging before production to avoid breaking applications.

βœ—

Placing all instances in public subnets. Only load balancers and NAT gateways should have public IPs; keep application and database servers in private subnets.

Stop Managing EC2 Security Manually

With dozens of instances and constantly changing configurations, manual security management becomes impossible. AWSight automatically monitors your EC2 security posture 24/7, detects misconfigurations, and provides real-time insights across your entire fleet.

References