Tarek Cheikh
Founder & AWS Security Expert
Amazon EC2 is the backbone of most AWS deployments. Whether you run web servers, application tiers, databases, or machine learning workloads, EC2 instances are high-value targets for attackers. A single misconfigured instance can serve as a beachhead for lateral movement, data exfiltration, cryptomining, or full account takeover.
In February 2026, the LexisNexis breach demonstrated the severity of EC2 misconfigurations. Attackers exploited React2Shell (a critical Server-Side Rendering RCE vulnerability) against an EC2-hosted application, pivoting from code execution to cloud credential theft via the instance metadata service. Throughout 2025, SSRF attacks targeting IMDSv1 remained one of the most reliable techniques for stealing temporary credentials from EC2 instances. In a separate campaign, attackers exploited exposed JupyterLab instances running on EC2 to hijack GPU resources for LLM inference (dubbed "LLMjacking"), racking up six-figure cloud bills in days.
This guide covers 12 battle-tested EC2 security best practices, each with real AWS CLI commands, audit procedures, and the latest 2025-2026 updates from AWS.
The EC2 Instance Metadata Service (IMDS) provides temporary credentials, instance identity documents, and user data to running instances. IMDSv1 responds to simple HTTP GET requests with no authentication, making it trivially exploitable via SSRF vulnerabilities. IMDSv2 requires a session token obtained through a PUT request with a hop limit, blocking most SSRF attacks.
Throughout 2025, SSRF-to-IMDS credential theft remained one of the most common EC2 attack patterns. The February 2026 LexisNexis breach used this exact chain: RCE via React2Shell, then credential harvesting from IMDSv1.
# Set account-level default to require IMDSv2 for all new instances
aws ec2 modify-instance-metadata-defaults --http-tokens required --http-put-response-hop-limit 1 --http-endpoint enabled
# Enforce IMDSv2 on an existing instance
aws ec2 modify-instance-metadata-options --instance-id i-0123456789abcdef0 --http-tokens required --http-put-response-hop-limit 1 --http-endpoint enabled
# Find all instances still using IMDSv1
aws ec2 describe-instances --query "Reservations[].Instances[?MetadataOptions.HttpTokens=='optional'].[InstanceId,Tags[?Key=='Name'].Value|[0]]" --output table
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "RequireIMDSv2",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}]
}
CIS Benchmark: Control 5.6 (ensure IMDSv2 is required on all instances). Security Hub: [EC2.8] EC2 instances should use IMDSv2.
Security groups are stateful firewalls at the instance level. The most dangerous misconfiguration is allowing inbound access from 0.0.0.0/0 on management ports (SSH/22, RDP/3389) or on all ports. Attackers continuously scan the entire IPv4 address space for open management ports.
0.0.0.0/0 or ::/0 on any port unless it is a public-facing load balancer on ports 80/443.# Find security groups with 0.0.0.0/0 ingress rules
aws ec2 describe-security-groups --query "SecurityGroups[?IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]].[GroupId,GroupName,IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']].{Port:FromPort,Proto:IpProtocol}]" --output table
# Remove an overly permissive rule
aws ec2 revoke-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 22 --cidr 0.0.0.0/0
# Reference another security group instead of CIDR
aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 5432 --source-group sg-0abcdef1234567890
CIS Benchmark: Control 5.2 (no ingress from 0.0.0.0/0 to port 22), Control 5.3 (no ingress from 0.0.0.0/0 to port 3389). Security Hub: [EC2.19] Security groups should not allow unrestricted access to high-risk ports.
EBS volumes can contain sensitive data -- databases, application state, credentials, logs. Unencrypted volumes can be snapshotted, shared, and exfiltrated. Enabling encryption by default ensures every new volume and snapshot is encrypted with no code changes required.
# Enable default EBS encryption in the current region
aws ec2 enable-ebs-encryption-by-default
# Set a custom CMK as the default EBS encryption key
aws ec2 modify-ebs-default-kms-key-id --kms-key-id arn:aws:kms:us-east-1:123456789012:key/mrk-EXAMPLE
# Verify default encryption is enabled
aws ec2 get-ebs-encryption-by-default
# Enable across all regions (script)
for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
echo "Enabling EBS encryption in $region"
aws ec2 enable-ebs-encryption-by-default --region "$region"
done
# Find unencrypted EBS volumes
aws ec2 describe-volumes --filters Name=encrypted,Values=false --query "Volumes[*].[VolumeId,State,Attachments[0].InstanceId]" --output table
CIS Benchmark: Control 2.2.1 (EBS default encryption enabled). Security Hub: [EC2.7] EBS default encryption should be enabled.
Hardcoded AWS access keys in EC2 applications are the number one credential exposure risk. Instance profiles automatically deliver temporary credentials via the metadata service -- they rotate every few hours and never appear in source code, environment variables, or configuration files.
# Create a role with least-privilege permissions
aws iam create-role --role-name MyAppRole --assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Attach only the permissions your application needs
aws iam attach-role-policy --role-name MyAppRole --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# Create and associate instance profile
aws iam create-instance-profile --instance-profile-name MyAppProfile
aws iam add-role-to-instance-profile --instance-profile-name MyAppProfile --role-name MyAppRole
# Attach to a running instance
aws ec2 associate-iam-instance-profile --instance-id i-0123456789abcdef0 --iam-instance-profile Name=MyAppProfile
Important: Combining instance profiles with IMDSv2 enforcement (Practice 1) ensures that even if an attacker achieves SSRF, the token-based metadata service blocks most credential theft attempts.
SSH requires open inbound ports, key management, and bastion hosts. AWS Systems Manager Session Manager provides shell access with zero inbound ports, IAM-based authentication, full keystroke logging to CloudWatch/S3, and integration with CloudTrail for session auditing.
# Start an interactive session (no SSH keys, no open ports)
aws ssm start-session --target i-0123456789abcdef0
# Enable session logging via Session Manager preferences
aws ssm update-document --name "SSM-SessionManagerRunShell" --document-version "$LATEST" --content '{
"schemaVersion": "1.0",
"description": "Session Manager Settings",
"sessionType": "Standard_Stream",
"inputs": {
"cloudWatchLogGroupName": "/aws/ssm/sessions",
"cloudWatchEncryptionEnabled": true,
"s3BucketName": "my-session-logs-bucket",
"s3EncryptionEnabled": true,
"kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/mrk-EXAMPLE"
}
}'
# Port forwarding through SSM (e.g., database access)
aws ssm start-session --target i-0123456789abcdef0 --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["5432"],"localPortNumber":["5432"]}'
Cost: Session Manager is free. Session logging to CloudWatch/S3 incurs standard storage and ingestion charges.
EC2 Instance Connect Endpoint (EIC Endpoint) provides secure, browser-based SSH/RDP connectivity to instances in private subnets without requiring a VPN, bastion host, or public IP address. It is a VPC-level resource that creates a private tunnel.
# Create an EC2 Instance Connect Endpoint in a private subnet
aws ec2 create-instance-connect-endpoint --subnet-id subnet-0123456789abcdef0 --security-group-ids sg-0123456789abcdef0
# Connect using the endpoint (no public IP needed)
aws ec2-instance-connect ssh --instance-id i-0123456789abcdef0
# Open an SSH tunnel for RDP or database access
aws ec2-instance-connect open-tunnel --instance-id i-0123456789abcdef0 --remote-port 3389 --local-port 3389
Key benefit: Unlike bastion hosts, EIC Endpoints have no instance to patch, no OS to harden, and no SSH keys to manage. Combined with SSM Session Manager (Practice 5), you can eliminate all inbound management ports across your fleet.
Instances in public subnets receive public IP addresses and are directly reachable from the internet. Even with strict security groups, a misconfigured rule or a zero-day vulnerability can lead to compromise. Private subnets provide network-level isolation -- instances can only be reached through controlled entry points (ALBs, NLBs, API Gateway, or VPN).
# Find instances with public IPs (potential candidates for migration)
aws ec2 describe-instances --query "Reservations[].Instances[?PublicIpAddress!=null].[InstanceId,PublicIpAddress,Tags[?Key=='Name'].Value|[0]]" --output table
# Create a VPC endpoint for S3 (eliminates need for internet access)
aws ec2 create-vpc-endpoint --vpc-id vpc-0123456789abcdef0 --service-name com.amazonaws.us-east-1.s3 --route-table-ids rtb-0123456789abcdef0
# Create an interface VPC endpoint for SSM (required for Session Manager in private subnets)
aws ec2 create-vpc-endpoint --vpc-id vpc-0123456789abcdef0 --vpc-endpoint-type Interface --service-name com.amazonaws.us-east-1.ssm --subnet-ids subnet-0123456789abcdef0 --security-group-ids sg-0123456789abcdef0
VPC endpoints needed for SSM in private subnets: ssm, ssmmessages, ec2messages (and optionally kms and logs for encrypted session logging).
VPC Flow Logs capture metadata about IP traffic flowing through your network interfaces. They are essential for detecting lateral movement, data exfiltration, port scanning, and communication with known malicious IPs. Without flow logs, you are blind to network-layer activity.
# Enable VPC Flow Logs (all traffic, delivered to CloudWatch Logs)
aws ec2 create-flow-logs --resource-type VPC --resource-ids vpc-0123456789abcdef0 --traffic-type ALL --log-destination-type cloud-watch-logs --log-group-name /aws/vpc/flowlogs --deliver-logs-permission-arn arn:aws:iam::123456789012:role/VPCFlowLogRole
# Enable VPC Flow Logs (S3 destination, Parquet format for Athena queries)
aws ec2 create-flow-logs --resource-type VPC --resource-ids vpc-0123456789abcdef0 --traffic-type ALL --log-destination-type s3 --log-destination arn:aws:s3:::my-flowlogs-bucket/vpc-logs/ --log-format '${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${tcp-flags}' --destination-options '{"FileFormat":"parquet","HiveCompatiblePartitions":true,"PerHourPartition":true}'
# Check which VPCs have flow logs enabled
aws ec2 describe-flow-logs --query "FlowLogs[*].[FlowLogId,ResourceId,LogDestination,TrafficType]" --output table
CIS Benchmark: Control 3.9 (VPC flow logging enabled in all VPCs). Security Hub: [EC2.6] VPC flow logging should be enabled in all VPCs. Cost optimization: Use S3 with Parquet format and per-hour partitioning for cost-effective long-term storage and fast Athena queries.
Amazon Inspector automatically discovers EC2 instances and scans them for software vulnerabilities (CVEs) and network exposure issues. It uses the SSM Agent for agentless scanning and integrates with Security Hub for centralized findings.
The 2025 LLMjacking campaign exploited known vulnerabilities in JupyterLab instances running on EC2 -- vulnerabilities that Inspector would have flagged with high-severity findings before exploitation.
# Enable Inspector for EC2 scanning
aws inspector2 enable --resource-types EC2
# Enable for the entire organization (delegated admin)
aws inspector2 enable --resource-types EC2 --account-ids "123456789012" "234567890123"
# Check Inspector coverage status
aws inspector2 list-coverage --filter-criteria '{"resourceType":[{"comparison":"EQUALS","value":"AWS_EC2_INSTANCE"}]}' --query "coveredResources[*].[resourceId,scanStatus.statusCode,scanStatus.reason]" --output table
# List critical and high-severity findings
aws inspector2 list-findings --filter-criteria '{
"severity": [{"comparison": "EQUALS", "value": "CRITICAL"}, {"comparison": "EQUALS", "value": "HIGH"}],
"resourceType": [{"comparison": "EQUALS", "value": "AWS_EC2_INSTANCE"}]
}' --query "findings[*].[title,severity,resourceId]" --output table
2025 Update: Inspector now supports agentless scanning for EC2 instances that cannot run the SSM Agent, CIS Benchmark scanning for operating system hardening, and deep inspection for application-layer vulnerabilities in Java and Node.js packages. Inspector also expanded Nitro Enclaves attestation scanning across all regions in October 2025.
Golden AMIs are pre-hardened, pre-approved base images that enforce a security baseline across your fleet. Without standardized AMIs, teams launch instances from community or marketplace AMIs that may contain outdated packages, default credentials, or embedded malware.
# Block public AMI sharing at the account level
aws ec2 enable-image-block-public-access --image-block-public-access-state block-new-sharing
# Verify the setting (should return "block-new-sharing")
aws ec2 get-image-block-public-access
# Find AMIs shared publicly from your account
aws ec2 describe-images --owners self --query "Images[?Public==`true`].[ImageId,Name,CreationDate]" --output table
# Create an Image Builder pipeline for hardened AMIs
aws imagebuilder create-image-pipeline --name "hardened-amazon-linux-2023" --image-recipe-arn arn:aws:imagebuilder:us-east-1:123456789012:image-recipe/hardened-al2023/1.0.0 --infrastructure-configuration-arn arn:aws:imagebuilder:us-east-1:123456789012:infrastructure-configuration/standard --schedule '{"scheduleExpression":"cron(0 8 ? * mon *)","pipelineExecutionStartCondition":"EXPRESSION_MATCH_AND_DEPENDENCY_UPDATES_AVAILABLE"}'
# Deprecate old AMIs
aws ec2 enable-image-deprecation --image-id ami-0123456789abcdef0 --deprecate-at "2026-06-01T00:00:00Z"
Security Hub: [EC2.25] EC2 launch templates should not assign public IPs. Use launch templates with AssociatePublicIpAddress: false to enforce private-only deployments. Use Image Builder pipelines to produce hardened, CIS-compliant AMIs on a regular schedule.
The EC2 serial console provides low-level text-based access to an instance's serial port for troubleshooting boot and network issues. While useful for break-glass scenarios, it bypasses network security controls and can be used for unauthorized access if IAM permissions are overly broad.
# Disable serial console access at the account level
aws ec2 disable-serial-console-access
# Verify serial console is disabled
aws ec2 get-serial-console-access-status
# SCP to prevent re-enabling serial console
# Include in your organization-wide deny policies
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenySerialConsoleAccess",
"Effect": "Deny",
"Action": [
"ec2:EnableSerialConsoleAccess",
"ec2-instance-connect:SendSerialConsoleSSHPublicKey"
],
"Resource": "*"
}]
}
Security Hub: [EC2.21] Network ACLs should not allow ingress from 0.0.0.0/0 to port 22 or port 3389. While distinct from serial console, both represent management access that should be tightly controlled.
EC2 user data is visible to anyone with ec2:DescribeInstanceAttribute permissions and is accessible from the instance metadata service. Placing database passwords, API keys, or private keys in user data is equivalent to storing them in plaintext.
# BAD: Never do this in user data
# #!/bin/bash
# export DB_PASSWORD="SuperSecret123!"
# export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
# GOOD: Retrieve secrets at runtime from Secrets Manager
aws secretsmanager get-secret-value --secret-id prod/myapp/database --query "SecretString" --output text
# GOOD: Retrieve from Parameter Store (SecureString)
aws ssm get-parameter --name /prod/myapp/api-key --with-decryption --query "Parameter.Value" --output text
# Audit: Check user data of existing instances for embedded secrets
aws ec2 describe-instance-attribute --instance-id i-0123456789abcdef0 --attribute userData --query "UserData.Value" --output text | base64 -d
Nitro Enclaves (October 2025 expansion): For the most sensitive secrets processing, AWS Nitro Enclaves -- now available across all regions as of October 2025 -- provide an isolated compute environment with no persistent storage, no network access, and cryptographic attestation. Use Enclaves for processing PII, financial data, or cryptographic key material.
| Misconfiguration | Risk | Detection |
|---|---|---|
| IMDSv1 enabled (HttpTokens: optional) | SSRF-based credential theft (LexisNexis breach, Feb 2026) | Security Hub: [EC2.8] |
| Security group with 0.0.0.0/0 on port 22 | Direct SSH brute-force from the internet | Security Hub: [EC2.19] |
| Unencrypted EBS volumes | Data exposure via snapshot sharing or physical access | Security Hub: [EC2.7] |
| Hardcoded access keys instead of instance profiles | Long-lived credentials in code, config, or env vars | IAM credential report, GuardDuty |
| Instances in public subnets with public IPs | Direct internet exposure increases attack surface | Security Hub: [EC2.25] |
| No VPC Flow Logs | Blind to network-layer attacks and exfiltration | Security Hub: [EC2.6] |
| Secrets in user data | Plaintext credentials accessible via metadata and API | Manual audit, custom Config rules |
| Public AMIs with sensitive data | Accidental exposure of proprietary software or credentials | aws ec2 describe-images --owners self --query "Images[?Public]" |
| # | Practice | Priority |
|---|---|---|
| 1 | Enforce IMDSv2 on all instances | Critical |
| 2 | Apply security groups with least privilege | Critical |
| 3 | Enable default EBS encryption in all regions | High |
| 4 | Use instance profiles instead of access keys | Critical |
| 5 | Replace SSH with SSM Session Manager | High |
| 6 | Deploy EC2 Instance Connect Endpoint | Medium |
| 7 | Place instances in private subnets | High |
| 8 | Enable VPC Flow Logs on all VPCs | High |
| 9 | Enable Amazon Inspector for continuous scanning | High |
| 10 | Harden AMIs and block public sharing | High |
| 11 | Disable EC2 serial console access | Medium |
| 12 | Never put secrets in user data | Critical |
This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Comprehensive guide to securing AWS Identity and Access Management. Covers MFA enforcement, least privilege, IAM Identity Center, SCPs, Access Analyzer, and credential management.
Comprehensive guide to securing AWS Lambda functions. Covers execution role least privilege, Function URL authentication, VPC placement, code signing, environment variable encryption, Secrets Manager integration, and SnapStart security considerations.
Comprehensive guide to securing Amazon S3. Covers Block Public Access, encryption (SSE-KMS, SSE-C deprecation), Object Lock, MFA Delete, VPC endpoints, presigned URLs, and GuardDuty S3 Protection.