Docs / Security Groups

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

1. When an EC2 instance is launched, LocalEmu translates its security groups into 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
2. Both chains allow ESTABLISHED,RELATED traffic (stateful, matching real AWS SG behavior) and DNS, then evaluate each rule in order
3. Every real EC2 attribute is honored: CIDR ranges, protocol + port ranges, and security-group-to-security-group references (resolved to the referenced group's member IPs)
4. If a matching allow rule exists, the packet is accepted. Otherwise it hits the chain's final DROP.
5. AuthorizeSecurityGroupIngress / Egress and Revoke... re-run apply_sg_to_container, so changes take effect immediately, no restart needed
6. Fail-closed: if the rules can't be applied (missing iptables, moto unreachable), the container gets a default-DROP policy rather than being left wide open

Supported Features

Feature Status Notes
CIDR matchingSupported0.0.0.0/0, 10.0.0.0/8, specific IPs, any valid CIDR
Protocol + port rangesSupportedTCP, UDP, protocol number, port ranges (e.g. 8000-9000)
Security group referencesSupportedAllow traffic from instances in another security group
Ingress rulesEnforcedEvaluated per connection, live updates
Egress rulesEnforcedReal 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 ACLsNot implementedOnly 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

Terminal
$ 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

Terminal
$ 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

Terminal
$ 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

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

Terminal
$ 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

Terminal
$ 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.

Terminal
$ 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.

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