How to allowlist AWS IP ranges

"Allow AWS service X in region Y through the firewall" sounds like a list-copying job. Done well, it is three decisions: use a list AWS maintains for you if one exists, pick exactly the right service code if it doesn't, and automate updates, because AWS changes its ranges frequently and without a fixed schedule.

An IP allowlist for a shared AWS service admits every AWS customer using that service, not just you. Allowing EC2 or AMAZON ranges lets in anything anyone runs on AWS. Treat IP rules as one layer, next to authentication.

Prefer AWS-managed prefix lists

If the rule lives in AWS (a security group, a route table), use an AWS-managed prefix list. AWS keeps it current, and you reference it by ID instead of pasting CIDRs. At the time of writing AWS offers them for:

ServicePrefix list name
CloudFront (origin-facing servers)com.amazonaws.global.cloudfront.origin-facing, com.amazonaws.global.ipv6.cloudfront.origin-facing
DynamoDBcom.amazonaws.region.dynamodb
EC2 Instance Connectcom.amazonaws.region.ec2-instance-connect, com.amazonaws.region.ipv6.ec2-instance-connect
Ground Stationcom.amazonaws.global.groundstation
Route 53 health checkscom.amazonaws.region.route53-healthchecks, com.amazonaws.region.ipv6.route53-healthchecks
S3com.amazonaws.region.s3
S3 Express One Zonecom.amazonaws.region.s3express
Secrets Manager (managed external secrets)com.amazonaws.region.secretsmanager-managed-external-secrets
VPC Latticecom.amazonaws.region.vpc-lattice, com.amazonaws.region.ipv6.vpc-lattice

Check AWS's current table before you build: it also gives each list's weight, the number of rules it counts as against your security group quota. The CloudFront list is heavy enough to crowd out other rules.

Prefix list IDs differ between Regions, so look them up by name:

PL_ID=$(aws ec2 describe-managed-prefix-lists --region eu-west-2 \
  --filters Name=prefix-list-name,Values=com.amazonaws.global.cloudfront.origin-facing \
  --query 'PrefixLists[0].PrefixListId' --output text)

# See what's in it
aws ec2 get-managed-prefix-list-entries --region eu-west-2 --prefix-list-id "$PL_ID"

# Allow HTTPS from it
aws ec2 authorize-security-group-ingress --region eu-west-2 \
  --group-id sg-0123456789abcdef0 \
  --ip-permissions "IpProtocol=tcp,FromPort=443,ToPort=443,PrefixListIds=[{PrefixListId=$PL_ID}]"

In Terraform:

data "aws_ec2_managed_prefix_list" "cloudfront" {
  name = "com.amazonaws.global.cloudfront.origin-facing"
}

resource "aws_vpc_security_group_ingress_rule" "https_from_cloudfront" {
  security_group_id = aws_security_group.origin.id
  prefix_list_id    = data.aws_ec2_managed_prefix_list.cloudfront.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
}

For AWS-to-AWS traffic, you may not need IP rules at all: gateway VPC endpoints for S3 and DynamoDB keep that traffic inside AWS's network.

Pick the right service code

Firewalls outside AWS need raw CIDRs from ip-ranges.json. Most mistakes start with the wrong code:

You want to…UseNot
Let CloudFront reach your originCLOUDFRONT_ORIGIN_FACINGCLOUDFRONT, which also covers the edge addresses viewers connect to
Let Route 53 health checks inROUTE53_HEALTHCHECKSROUTE53_HEALTHCHECKS_PUBLISHING, which Route 53 uses only internally
Allow EC2 Instance Connect from the consoleEC2_INSTANCE_CONNECT for your instance's regionAll of EC2
Accept calls from API Gateway integrationsAPI_GATEWAY (outbound-only addresses)Those ranges as a destination: API endpoints aren't hosted on them
Let servers reach S3 in one RegionS3 for that Region; from inside a VPC, a gateway endpoint is simplerAMAZON
Query Route 53 name serversROUTE53Health check ranges

Filter ip-ranges.json with jq

curl -sO https://ip-ranges.amazonaws.com/ip-ranges.json

# IPv4 and IPv6 ranges for one service in one Region
jq -r '(.prefixes[]      | select(.service=="S3" and .region=="eu-west-1") | .ip_prefix),
       (.ipv6_prefixes[] | select(.service=="S3" and .region=="eu-west-1") | .ipv6_prefix)' ip-ranges.json

# EC2 ranges that aren't used by S3 (a range tagged S3 and EC2 belongs to S3)
jq -r '[.prefixes[] | select(.service=="S3") | .ip_prefix] as $s3
  | .prefixes[] | select(.service=="EC2" and .region=="us-east-1") | .ip_prefix
  | select(IN($s3[]) | not)' ip-ranges.json

# Load a list into an ipset
ipset create aws-s3-euw1 hash:net family inet
jq -r '.prefixes[] | select(.service=="S3" and .region=="eu-west-1") | .ip_prefix' ip-ranges.json \
  | xargs -n1 ipset add aws-s3-euw1

Filtering on .region includes the Region's Local Zones and Wavelength Zones. To get only the Region itself, filter on .network_border_group instead. To merge adjacent ranges into fewer rules, Python's standard library can collapse them:

jq -r '.prefixes[] | select(.service=="S3" and .region=="eu-west-1") | .ip_prefix' ip-ranges.json \
  | python3 -c 'import ipaddress, sys; [print(n) for n in ipaddress.collapse_addresses(ipaddress.ip_network(l.strip()) for l in sys.stdin)]'

Build a list in your browser

This builder does the same filtering on the current ip-ranges.json. It downloads the file from AWS only when you click, and it can output plain CIDRs, nginx set_real_ip_from lines or a JSON array.

Keep lists current with SNS

AWS announces every change to ip-ranges.json on an Amazon SNS topic in us-east-1:

arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged

Subscribe an SQS queue, a Lambda function or an email address:

aws sns subscribe --region us-east-1 \
  --topic-arn arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged \
  --protocol sqs --notification-endpoint arn:aws:sqs:us-east-1:123456789012:ip-ranges-changed

For SQS, the queue also needs an access policy that allows sns.amazonaws.com to send messages (sqs:SendMessage) when aws:SourceArn is arn:aws:sns:us-east-1:806199016981:AmazonIpSpaceChanged. Without it, the subscription is created but notifications never arrive.

Each message carries create-time, synctoken, md5 and url. An update job should:

  1. download the file and check its syncToken and MD5 (md5sum ip-ranges.json) against the message; a mismatch usually means a newer version is already out;
  2. ignore messages older than the version you already applied, because notifications can arrive out of order;
  3. filter for your service and Region, and compare with the current list. Most updates don't touch the ranges you care about;
  4. refuse to apply an empty or suspiciously short list, and keep the last good one;
  5. also run on a schedule, because AWS notes that delivery depends on the endpoint being available.

Services that don't publish ranges

AWS doesn't publish ranges for every service. The common gaps:

Common mistakes

Sources