ComputeIntermediate20 min read

    AWS EC2 Security Best Practices

    Tarek Cheikh

    Founder & AWS Security Expert

    View Security Card

    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.

    1. Enforce IMDSv2 on All Instances

    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.

    Implementation

    • Set account-level defaults so every new instance launches with IMDSv2 required.
    • Use SCPs to prevent anyone from launching instances with IMDSv1.
    • Use Organization Declarative Policies (introduced December 2024) to enforce IMDSv2 as a non-overridable organization-wide default.
    # 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

    SCP to Block IMDSv1 Launches

    {
      "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.

    2. Apply Security Groups with Least Privilege

    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.

    Implementation

    • Never allow 0.0.0.0/0 or ::/0 on any port unless it is a public-facing load balancer on ports 80/443.
    • Reference other security groups instead of CIDR ranges for internal communication.
    • Use VPC security group descriptions to document the business justification for each rule.
    # 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.

    3. Enable Default EBS Encryption in All Regions

    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.

    Implementation

    • Enable default encryption in every region, including regions you do not actively use (to prevent shadow deployments).
    • Use a customer-managed KMS key (CMK) for control over key policies, rotation, and cross-account sharing.
    # 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.

    4. Use Instance Profiles Instead of Access Keys

    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.

    Implementation

    # 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.

    5. Replace SSH with SSM Session Manager

    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.

    Implementation

    • Install the SSM Agent (pre-installed on Amazon Linux 2023, Ubuntu 20.04+ AMIs).
    • Remove port 22 from all security groups.
    • Enable session logging to CloudWatch Logs and S3 for compliance.
    # 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.

    6. Deploy EC2 Instance Connect Endpoint for Private Subnet Access

    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.

    Implementation

    # 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.

    7. Place Instances in Private Subnets

    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).

    Architecture Pattern

    • Public subnets: Only load balancers, NAT gateways, and bastion hosts (if you still use them).
    • Private subnets: All EC2 application instances, databases, and internal services.
    • Outbound access: Use NAT gateways or VPC endpoints for internet and AWS service access.
    # 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).

    8. Enable VPC Flow Logs on All VPCs

    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.

    Implementation

    # 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.

    9. Enable Amazon Inspector for Continuous Vulnerability Scanning

    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.

    Implementation

    # 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.

    10. Harden AMIs and Block Public Sharing

    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.

    Implementation

    • Build golden AMIs using EC2 Image Builder with CIS Benchmark hardening pipelines.
    • Block public AMI sharing at the account level to prevent accidental exposure.
    • Tag and version AMIs for traceability and automatic deprecation.
    # 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.

    11. Disable EC2 Serial Console Access

    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.

    Implementation

    # 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

    SCP to Prevent Re-Enabling Serial Console

    {
      "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.

    12. Never Put Secrets in User Data

    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.

    Secure Alternatives

    • AWS Secrets Manager for database credentials, API keys, and other secrets with automatic rotation.
    • AWS Systems Manager Parameter Store (SecureString type) for configuration values with KMS encryption.
    • Instance profiles (Practice 4) for AWS API access -- no credentials needed at all.
    # 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.


    Common Misconfigurations

    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]"

    Quick Reference Checklist

    # Practice Priority
    1Enforce IMDSv2 on all instancesCritical
    2Apply security groups with least privilegeCritical
    3Enable default EBS encryption in all regionsHigh
    4Use instance profiles instead of access keysCritical
    5Replace SSH with SSM Session ManagerHigh
    6Deploy EC2 Instance Connect EndpointMedium
    7Place instances in private subnetsHigh
    8Enable VPC Flow Logs on all VPCsHigh
    9Enable Amazon Inspector for continuous scanningHigh
    10Harden AMIs and block public sharingHigh
    11Disable EC2 serial console accessMedium
    12Never put secrets in user dataCritical

    Related Resources

    Go Deeper: The State of AWS Security 2026

    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.

    EC2IMDSv2Security GroupsEBS EncryptionSSMVPCInspector