IPv6 addresses are globally routable by default, so an IPv6-enabled subnet with a plain internet gateway route is a subnet whose workloads can be dialled from the internet. An egress-only internet gateway is the IPv6 equivalent of “outbound only”: stateful, free, and the correct answer for a service whose only job is to call a model API.
What it is and what it costs
Amazon describes it as “a horizontally scaled, redundant, and highly available VPC component that allows outbound communication over IPv6 from instances in your VPC to the internet, and prevents the internet from initiating an IPv6 connection with your instances” — from the AWS egress-only internet gateway documentation. Three properties follow from that and each one matters:
- IPv6 only. AWS states it directly: for outbound-only communication over IPv4, use a NAT gateway instead. An egress-only gateway does nothing for IPv4 traffic, and a dual-stack subnet needs both.
- Stateful. It forwards traffic out and sends the response back, so a normal request/response API call works without any inbound rule.
- Free. AWS states there is no charge for an egress-only internet gateway, though data transfer charges for the instances using it still apply. That is a genuine difference from a NAT gateway’s hourly and per-GB charges, and for a high-throughput model-calling service it is not a small one.
There is no address translation involved, which is the conceptual difference from NAT. Your instance keeps its own globally unique IPv6 address and the provider sees that address, not a shared one. For allowlisting that is a mixed blessing: the address is stable and specific, but it is per-instance rather than per-gateway, so a provider allowlist has to cover the subnet prefix rather than a single address. Plan the subnet allocation accordingly — allowlisting a provider’s IP ranges covers the reverse direction.
Creating it and routing to it
Two calls. One gateway per VPC — AWS notes you can attach only one at a time — and a route for all internet-bound IPv6 traffic.
EIGW=$(aws ec2 create-egress-only-internet-gateway \
--vpc-id vpc-0abc \
--query EgressOnlyInternetGateway.EgressOnlyInternetGatewayId --output text)
aws ec2 create-route \
--route-table-id rtb-0private1a \
--destination-ipv6-cidr-block ::/0 \
--egress-only-internet-gateway-id "$EIGW"
The Terraform equivalent, since the route is easy to omit by hand:
resource "aws_egress_only_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route" "private_ipv6_default" {
route_table_id = aws_route_table.private_1a.id
destination_ipv6_cidr_block = "::/0"
egress_only_gateway_id = aws_egress_only_internet_gateway.main.id
}
The subnet also needs an IPv6 CIDR and the workload needs an address from it. For ECS tasks and EC2 instances that means the subnet attribute AssignIpv6AddressOnCreation, or explicitly requesting an address at launch. A subnet with an IPv6 CIDR whose instances have no IPv6 address will route IPv4 as before and give no sign that the gateway you just built is unused.
No security group, so use a network ACL
AWS is explicit that you cannot associate a security group with an egress-only internet gateway, and that a network ACL is the control available for traffic to and from the subnet it serves. This is worth internalising, because it inverts the usual instinct: with a NAT gateway most people control egress at the instance’s security group, and that still works here — the security group on the workload is where you restrict outbound to 443. The gateway itself simply has no policy surface.
Remember that a network ACL is stateless. An outbound rule permitting TCP 443 to ::/0 needs a matching inbound rule permitting the ephemeral port range back, or the response never arrives and every call hangs. This is the standard NACL trap and IPv6 rules are counted separately from IPv4 ones against the per-ACL rule quota.
When the provider is IPv4-only
Here is the case the documentation does not connect for you, and it is the one that actually decides whether this design works. An IPv6-only workload can only send and receive IPv6 packets. If the model API you call publishes no AAAA record, a DNS lookup returns an IPv4 address your workload cannot use, and the egress-only gateway is irrelevant because there was never an IPv6 destination to route to.
Check before you commit to the topology:
dig +short AAAA api.anthropic.com
dig +short AAAA bedrock-runtime.us-east-1.amazonaws.com
Anthropic, for one, publishes an inbound IPv6 range of 2607:6bc0::/48 on its IP addresses page, so an IPv6 path to it exists. Many APIs still have no AAAA record at all.
When there is no AAAA record, AWS’s answer is DNS64 with NAT64, and it is a genuinely neat one. Enable DNS64 on the subnet and Route 53 Resolver synthesises an IPv6 address for an IPv4-only destination by prepending the well-known prefix from RFC 6052, 64:ff9b::/96, to the IPv4 address. You then route that prefix to a NAT gateway, which recognises it, strips the prefix and translates to IPv4. AWS notes NAT64 is automatically available on every NAT gateway and is not a feature you enable.
# route the synthesised prefix at the NAT gateway
aws ec2 create-route --route-table-id rtb-0private1a \
--destination-ipv6-cidr-block 64:ff9b::/96 \
--nat-gateway-id nat-05dba92075d71c408
# keep genuine IPv6 traffic on the free, outbound-only path
aws ec2 create-route --route-table-id rtb-0private1a \
--destination-ipv6-cidr-block ::/0 \
--egress-only-internet-gateway-id "$EIGW"
aws ec2 modify-subnet-attribute --subnet-id subnet-0private1a --enable-dns64
The ordering of those two routes is not a choice you make — longest prefix wins, so 64:ff9b::/96 takes precedence over ::/0 automatically. Providers with AAAA records go out through the free egress-only gateway; IPv4-only providers go through the NAT gateway and are charged accordingly. AWS also notes to permit 64:ff9b::/96 in security groups and NACLs, which is easy to forget because it does not look like an internet destination.
AWS documents ::/0 pointed at a plain internet gateway as an alternative here, and warns that doing so allows external IPv6 hosts to initiate connections into your VPC. That is exactly what the egress-only gateway exists to prevent, so it is the wrong swap for this use case even though it appears in the same walkthrough.
Confirming the direction is one-way
- From the workload, confirm outbound works over IPv6:
curl -6 -sS -o /dev/null -w '%{http_code}\n' https://api.anthropic.com/v1/messages. A 401 means the path is complete. - Confirm the source address is the instance’s own global IPv6 address, not a translated one:
curl -6 -sS https://checkip.amazonaws.com. - From outside AWS, attempt an inbound connection to that address on any port. It should time out. If it does not, you have a route to a plain internet gateway somewhere in the table, not an egress-only one.
- Check the route table has no
::/0entry targetingigw-. A leftover from an earlier configuration wins or loses unpredictably depending on which was created first, and it defeats the entire arrangement silently.
That third step is the only one that proves the property you built this for. Everything else confirms the service works; only an attempted inbound connection confirms that nothing else can reach it.
Top comments (0)