← Back to DevBytes

EC2 Security: IAM Policies and Network Security

Understanding EC2 Security: IAM Policies and Network Security

When you launch an EC2 instance in AWS, you're not just spinning up a virtual machine—you're placing a critical asset into a complex cloud environment where threats can come from anywhere. Securing your EC2 instances requires mastering two complementary security layers: IAM Policies (who can do what) and Network Security (who can reach what). Together, these form a defense-in-depth strategy that protects both the control plane and the data plane of your infrastructure.

The Two Pillars of EC2 Security

Think of EC2 security as two concentric rings of protection. The outer ring is network security—security groups, network ACLs, and VPC design—which controls network-level access to your instances. The inner ring is IAM policies, which govern what actions users, roles, and services can perform on your EC2 resources. A properly secured EC2 environment requires both rings to be hardened. Network security alone won't stop someone with overly permissive IAM credentials from deleting an instance via the AWS CLI. Conversely, perfect IAM policies won't block an attacker from exploiting an open port if your security group allows traffic from 0.0.0.0/0.

IAM Policies for EC2: What They Are

IAM (Identity and Access Management) policies are JSON documents that define permissions for identities (users, groups, roles) or resources in AWS. For EC2 specifically, IAM policies control:

Why IAM Policies Matter for EC2

Without strict IAM policies, your AWS environment is vulnerable to privilege escalation, accidental resource deletion, and credential misuse. Consider a scenario: a developer has overly broad permissions and accidentally terminates a production instance, or a compromised CI/CD pipeline with full EC2 access is used to launch crypto-mining instances. Proper IAM policies implement the principle of least privilege, ensuring every identity has exactly the permissions it needs—nothing more. This limits the blast radius of any security incident and makes your environment auditable and compliant.

How to Use IAM Policies with EC2

There are two primary ways IAM policies interact with EC2: identity-based policies attached to users/roles that control what they can do to EC2, and resource-based policies or instance profiles that grant the EC2 instance itself permissions to access other AWS services.

Example 1: Restricting a Developer to Specific EC2 Actions

The following IAM policy allows a developer to list, start, and stop instances—but only those tagged with Environment: Development. They cannot terminate instances or modify production resources.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowEC2ListAll",
      "Effect": "Allow",
      "Action": "ec2:Describe*",
      "Resource": "*"
    },
    {
      "Sid": "AllowStartStopDevInstances",
      "Effect": "Allow",
      "Action": [
        "ec2:StartInstances",
        "ec2:StopInstances",
        "ec2:RebootInstances"
      ],
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringEquals": {
          "ec2:ResourceTag/Environment": "Development"
        }
      }
    },
    {
      "Sid": "DenyTerminateAny",
      "Effect": "Deny",
      "Action": "ec2:TerminateInstances",
      "Resource": "*"
    }
  ]
}

Notice the Deny statement explicitly blocks termination—this overrides any other allow rules. The Condition block uses tag-based access control, a powerful pattern for segmenting permissions by environment, team, or project.

Example 2: Instance Profile for EC2 to Access S3

When your EC2 instance needs to read from an S3 bucket, you attach an instance profile (which wraps an IAM role) to the instance. The following policy grants read-only access to a specific bucket. The application running on the instance uses the AWS SDK and automatically receives temporary credentials via the instance metadata service.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-app-bucket",
        "arn:aws:s3:::my-app-bucket/*"
      ]
    }
  ]
}

To attach this role to an existing instance using the AWS CLI:

# Create the instance profile (if not already created)
aws iam create-instance-profile --instance-profile-name MyAppInstanceProfile

# Add the role to the instance profile
aws iam add-role-to-instance-profile \
  --instance-profile-name MyAppInstanceProfile \
  --role-name MyAppS3ReadOnlyRole

# Attach the instance profile to an existing EC2 instance
aws ec2 associate-iam-instance-profile \
  --instance-id i-0abc123def4567890 \
  --iam-instance-profile Name=MyAppInstanceProfile

Example 3: Enforcing MFA for Destructive Actions

This policy requires multi-factor authentication before allowing instance termination, adding an extra layer of protection against accidental or compromised-credential deletions.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ec2:*",
      "Resource": "*"
    },
    {
      "Effect": "Deny",
      "Action": "ec2:TerminateInstances",
      "Resource": "*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    }
  ]
}

Network Security for EC2: What It Is

Network security in AWS EC2 operates at multiple layers within your VPC (Virtual Private Cloud). The two primary mechanisms are Security Groups (stateful firewalls at the instance/ENI level) and Network ACLs (stateless firewalls at the subnet level). Together with proper VPC design—public/private subnets, NAT gateways, and route tables—these controls define which network traffic can reach your instances and which traffic can leave them.

Security Groups act as virtual firewalls for your EC2 instances. They are stateful, meaning if you allow inbound traffic on port 80, the corresponding outbound response traffic is automatically allowed regardless of outbound rules. They only support allow rules—you cannot explicitly deny traffic with security groups. Every rule specifies a protocol, port range, and source (CIDR block, another security group, or a prefix list).

Network ACLs (NACLs) are subnet-level, stateless firewalls that support both allow and deny rules. Because they are stateless, you must explicitly allow both inbound and outbound traffic for a connection to work (e.g., allow inbound on port 80 and outbound on ephemeral ports). NACLs are evaluated in rule-number order, and the first matching rule wins.

Why Network Security Matters

Network security is your first line of defense against unauthorized access, port scanning, DDoS attacks, and lateral movement within your VPC. A misconfigured security group exposing SSH (port 22) to 0.0.0.0/0 is one of the most common causes of EC2 compromise—bots constantly scan for open SSH ports and attempt brute-force attacks. Beyond external threats, proper network segmentation ensures that even if one instance is compromised, attackers cannot easily reach your database or other internal services. Network security also enables compliance with frameworks like PCI DSS, HIPAA, and SOC 2, all of which require strict network isolation and access controls.

How to Configure Network Security

Security Groups: Practical Examples

Example 1: Web Server Security Group — Allows HTTP/HTTPS from anywhere, SSH only from a specific office IP, and all outbound traffic.

# Create a security group for web servers
aws ec2 create-security-group \
  --group-name WebServerSG \
  --description "Security group for public web servers" \
  --vpc-id vpc-0abcd1234efgh5678

# Allow inbound HTTP from anywhere
aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123def456789 \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

# Allow inbound HTTPS from anywhere
aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123def456789 \
  --protocol tcp --port 443 --cidr 0.0.0.0/0

# Allow inbound SSH only from a trusted IP range
aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123def456789 \
  --protocol tcp --port 22 --cidr 203.0.113.0/24

# (Optional) Add a description tag for auditing
aws ec2 create-tags \
  --resources sg-0abc123def456789 \
  --tags Key=Purpose,Value=PublicWebTraffic

Example 2: Database Security Group Referencing Another Security Group — One of the most powerful features: instead of specifying IP addresses, you reference another security group. This allows any instance in the referenced group to access your database, regardless of their IP.

# Allow MySQL access from any instance that has the WebServerSG security group
aws ec2 authorize-security-group-ingress \
  --group-id sg-0def456abc789012 \
  --protocol tcp --port 3306 \
  --source-group sg-0abc123def456789

# This means: "if traffic comes from an instance with sg-0abc123def456789,
# allow it on port 3306" — no IP addresses needed

In Terraform, this pattern looks like:

resource "aws_security_group" "web_sg" {
  name        = "WebServerSG"
  description = "Web server security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["203.0.113.0/24"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "db_sg" {
  name        = "DatabaseSG"
  description = "Database security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.web_sg.id]
    # Only instances with web_sg can connect to MySQL
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Network ACLs: Adding Stateless Subnet-Level Rules

While security groups handle most use cases, NACLs add a subnet-level barrier useful for explicit deny rules, compliance requirements, or trapping misconfigured instances. Here's a NACL configuration for a public-facing subnet that explicitly blocks SSH from outside while allowing HTTP/HTTPS:

# Create a Network ACL
aws ec2 create-network-acl --vpc-id vpc-0abcd1234efgh5678

# Inbound rules (rule number determines evaluation order)
# Rule 100: Allow HTTP from anywhere
aws ec2 create-network-acl-entry \
  --network-acl-id acl-0abc123def456 \
  --rule-number 100 \
  --protocol tcp --port-range From=80,To=80 \
  --rule-action allow \
  --cidr-block 0.0.0.0/0 \
  --ingress

# Rule 110: Allow HTTPS from anywhere
aws ec2 create-network-acl-entry \
  --network-acl-id acl-0abc123def456 \
  --rule-number 110 \
  --protocol tcp --port-range From=443,To=443 \
  --rule-action allow \
  --cidr-block 0.0.0.0/0 \
  --ingress

# Rule 120: Allow ephemeral ports for return traffic (32768-65535)
aws ec2 create-network-acl-entry \
  --network-acl-id acl-0abc123def456 \
  --rule-number 120 \
  --protocol tcp --port-range From=32768,To=65535 \
  --rule-action allow \
  --cidr-block 0.0.0.0/0 \
  --ingress

# Rule 200: Explicitly DENY SSH from everywhere
aws ec2 create-network-acl-entry \
  --network-acl-id acl-0abc123def456 \
  --rule-number 200 \
  --protocol tcp --port-range From=22,To=22 \
  --rule-action deny \
  --cidr-block 0.0.0.0/0 \
  --ingress

# Rule * (asterisk): Catch-all deny for unmatched traffic (default rule)
# Automatically exists — no need to create, but good to know it's there

# Outbound rules: Allow all outbound traffic
aws ec2 create-network-acl-entry \
  --network-acl-id acl-0abc123def456 \
  --rule-number 100 \
  --protocol -1 --port-range From=1,To=65535 \
  --rule-action allow \
  --cidr-block 0.0.0.0/0 \
  --egress

# Associate NACL with subnet
aws ec2 associate-network-acl \
  --network-acl-id acl-0abc123def456 \
  --subnet-id subnet-0sub12345net67890

Because NACLs are stateless, even with an allow inbound rule, the outbound response must also be explicitly allowed. The inbound ephemeral ports rule (120) ensures that responses to outbound-initiated connections can return. The explicit SSH deny rule (200) acts as a backstop—even if someone accidentally adds SSH to a security group, the NACL blocks it at the subnet boundary.

Combining IAM and Network Security: A Real-World Architecture

Let's walk through a production-ready architecture that layers IAM and network controls. Imagine a three-tier application with web servers, application servers, and a database, all running on EC2 within a VPC.

Network Layer:

IAM Layer:

Best Practices for EC2 Security

IAM Policy Best Practices

Network Security Best Practices

Operational Security Best Practices

Automated Security Group Audit Script

Here's a practical Python script using boto3 to audit your security groups for overly permissive rules. Run this regularly in your CI/CD pipeline or as a scheduled Lambda function.

import boto3

def audit_security_groups():
    ec2 = boto3.client('ec2')
    security_groups = ec2.describe_security_groups()['SecurityGroups']
    
    violations = []
    
    for sg in security_groups:
        for rule in sg.get('IpPermissions', []):
            for ip_range in rule.get('IpRanges', []):
                cidr = ip_range.get('CidrIp', '')
                port_range = f"{rule.get('FromPort', 'ALL')}-{rule.get('ToPort', 'ALL')}"
                
                # Check for dangerous open ports to the world
                dangerous_ports = {22: 'SSH', 3389: 'RDP', 3306: 'MySQL', 
                                   5432: 'PostgreSQL', 27017: 'MongoDB'}
                
                from_port = rule.get('FromPort', 0)
                
                if cidr == '0.0.0.0/0' and from_port in dangerous_ports:
                    violations.append({
                        'SecurityGroupId': sg['GroupId'],
                        'SecurityGroupName': sg['GroupName'],
                        'Port': from_port,
                        'Service': dangerous_ports[from_port],
                        'Cidr': cidr,
                        'Severity': 'CRITICAL'
                    })
                
                # Check for any port open to the world
                if cidr == '0.0.0.0/0' and from_port not in [80, 443, 0]:
                    violations.append({
                        'SecurityGroupId': sg['GroupId'],
                        'SecurityGroupName': sg['GroupName'],
                        'Port': from_port,
                        'Service': 'Custom',
                        'Cidr': cidr,
                        'Severity': 'HIGH'
                    })
    
    # Report violations
    if violations:
        print(f"Found {len(violations)} security group violations:")
        for v in violations:
            print(f"[{v['Severity']}] SG: {v['SecurityGroupName']} ({v['SecurityGroupId']}) - "
                  f"Port {v['Port']} ({v['Service']}) open to {v['Cidr']}")
    else:
        print("No security group violations found.")
    
    return violations

if __name__ == '__main__':
    audit_security_groups()

Enforcing IMDSv2 on Instance Launch

When launching instances via the CLI or SDK, always enforce IMDSv2 to protect against SSRF attacks that could leak instance profile credentials:

aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --security-group-ids sg-0abc123def456789 \
  --subnet-id subnet-0sub12345net67890 \
  --iam-instance-profile Name=MyAppInstanceProfile \
  --metadata-options HttpTokens=required,HttpPutResponseHopLimit=2,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Environment,Value=Production}]' \
  --user-data file://bootstrap.sh

The critical option is HttpTokens=required, which forces the instance metadata service to require a session token (v2), rendering simple SSRF attacks ineffective. The HttpPutResponseHopLimit=2 limits how far the token can travel, preventing container escape scenarios in Docker/Kubernetes environments.

Conclusion

EC2 security is not a single checkbox—it's a continuous practice built on the dual foundations of IAM policies and network security. IAM ensures that only the right identities can perform the right actions on your instances, while security groups and NACLs ensure that only the right network traffic can reach them. By combining tag-based IAM conditions, instance profiles instead of hardcoded credentials, security group referencing, explicit NACL deny rules, and operational practices like IMDSv2 enforcement and automated auditing, you create a defense-in-depth posture that protects your EC2 workloads against both external attackers and internal misconfigurations. Start with least privilege, enforce MFA for destructive actions, close every unnecessary port, and audit relentlessly—because in the cloud, security is not a destination but an ongoing journey of hardening and vigilance.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles