Security Groups
EC2 security group rules are enforced with real iptables chains inside each instance's own container. No restarts needed, and no host-level proxy sits in front of ordinary traffic.
How It Works
iptables rules and applies them inside the instance's own container: an INPUT chain (SG_IN) for ingress and an OUTPUT chain (SG_OUT) for egress ESTABLISHED,RELATED traffic (stateful, matching real AWS SG behavior) and DNS, then evaluate each rule in order AuthorizeSecurityGroupIngress / Egress and Revoke... re-run apply_sg_to_container, so changes take effect immediately, no restart needed iptables, moto unreachable), the container gets a default-DROP policy rather than being left wide open Supported Features
| Feature | Status | Notes |
|---|---|---|
| CIDR matching | Supported | 0.0.0.0/0, 10.0.0.0/8, specific IPs, any valid CIDR |
| Protocol + port ranges | Supported | TCP, UDP, protocol number, port ranges (e.g. 8000-9000) |
| Security group references | Supported | Allow traffic from instances in another security group |
| Ingress rules | Enforced | Evaluated per connection, live updates |
| Egress rules | Enforced | Real iptables OUTPUT chain; outbound looks unrestricted only because AWS's own default egress rule is allow-all -- revoke it and egress is actually blocked |
| VPC network ACLs | Not implemented | Only security groups are enforced |
Default Behavior
When an instance has no security group attached, all traffic is allowed. This preserves backward compatibility with existing workflows that do not use security groups. Once you attach a security group, only traffic matching the ingress rules is permitted.
Note: traffic source is the real caller
Because enforcement happens inside the instance's own container via iptables, rules are matched against the real source of the traffic (another container's IP, or -- for an EIP-associated instance -- the real caller's IP via the host-side proxy). There is no shared 127.0.0.1 to special-case: write CIDRs the same way you would against real AWS.
Example: Allow and Revoke SSH
Step 1: Create a security group
$ awsemu ec2 create-security-group \
--group-name my-sg \
--description "Allow SSH" \
--vpc-id vpc-12345
GroupId: sg-abc123 Step 2: Allow SSH from anywhere
$ awsemu ec2 authorize-security-group-ingress \
--group-id sg-abc123 \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
SecurityGroupRuleId: sgr-001 Step 3: Launch an instance with the security group, and get its PrivateIpAddress
$ awsemu ec2 run-instances \
--image-id ami-ubuntu-22.04 \
--instance-type t2.micro \
--key-name my-key \
--security-group-ids sg-abc123
InstanceId: i-abc12345
$ awsemu ec2 describe-instances --instance-ids i-abc12345 \
--query 'Reservations[0].Instances[0].PrivateIpAddress' --output text
10.0.1.15 LocalEmu does not open host-to-VPC-private-IP reachability on macOS by design, so SSH has to come from a container on the same VPC network (see EC2: API reference for the full bastion-container setup).
Step 4: SSH works because the rule allows port 22
# from a container on the same VPC network:
$ ssh -i my-key.pem ubuntu@10.0.1.15
Welcome to Ubuntu 22.04
ubuntu@i-abc12345:~$ Step 5: Revoke the SSH rule
$ awsemu ec2 revoke-security-group-ingress \
--group-id sg-abc123 \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0
Return: true Step 6: SSH is now blocked immediately
$ ssh -i my-key.pem ubuntu@10.0.1.15
ssh: connect to host 10.0.1.15 port 22: Operation timed out
# DROP silently discards the packet, so this hangs to a timeout
# rather than an immediate "Connection refused". No restart required. apply_sg_to_container re-applies the iptables rules the moment the rule changes.
Security Group References
You can reference another security group as the source instead of a CIDR block. This allows traffic only from instances that belong to the referenced group.
$ awsemu ec2 authorize-security-group-ingress \
--group-id sg-abc123 \
--protocol tcp \
--port 3306 \
--source-group sg-web-tier
# Only instances in sg-web-tier can reach port 3306 ENI-Level Live Re-Apply
Terraform and CDK generate ENI-level calls, not instance-level ones: AttachNetworkInterface, DetachNetworkInterface, and ModifyNetworkInterfaceAttribute --groups=.... LocalEmu watches all three: resolve_union_sgs_for_instance computes the union of security group IDs across every ENI currently attached to the instance and re-runs the same apply_sg_to_container step described above, so the container's iptables chains reflect the new posture immediately. Attaching a second ENI with a different security group, or changing an already-attached ENI's groups, takes effect without a restart or re-launch, exactly like AuthorizeSecurityGroupIngress does for a single ENI.
# Instance i-abc12345 is running with only sg-A attached (allows :8010)
$ awsemu ec2 create-network-interface \
--subnet-id subnet-12345 --groups sg-B
NetworkInterfaceId: eni-67890
$ awsemu ec2 attach-network-interface \
--instance-id i-abc12345 \
--network-interface-id eni-67890 \
--device-index 1
# :8020 (allowed by sg-B) is now reachable -- no restart, no re-launch $ awsemu ec2 modify-network-interface-attribute \
--network-interface-id eni-67890 \
--groups sg-A
# eni-67890 now carries sg-A instead of sg-B -- :8020 is DROPed again,
# the union of SGs across every attached ENI is re-applied immediately
Gated behind two flags, both on by default: LOCALEMU_ENI_REAL (makes ENI operations apply real network changes instead of metadata-only responses) and its prerequisite LOCALEMU_VPC_IP_PINNING (pins each ENI to a real IP inside its subnet's CIDR). You only need to touch these if you want to opt back into the old metadata-only behavior, by setting either to a falsy value -- see the Environment Variables reference.