ping6.net

IPv6 on AWS: VPC, EC2, ELB, and Beyond

Complete guide to deploying IPv6 on Amazon Web Services. Configure VPCs, subnets, security groups, load balancers, and egress-only gateways.

ping6.netDecember 14, 202413 min read
IPv6AWScloudVPCEC2networking

AWS has comprehensive IPv6 support across most services, but it's not enabled by default. You have to opt in, and the configuration is spread across multiple layers.

TL;DR - Quick Summary

Key Points:

  • AWS provides dual-stack IPv6 (not IPv6-only for most services)
  • You get Amazon-assigned /56 blocks from the 2600::/12 range
  • All IPv6 addresses are globally unique and routable (no NAT)
  • Egress-only gateways provide stateful filtering without address translation

Skip to: VPC Configuration | EC2 Instances | Security Groups | Load Balancers | Terraform Example

AWS IPv6 Architecture Overview#

AWS provides IPv6 in a dual-stack configuration. Your resources get both IPv4 and IPv6 addresses. You can't run IPv6-only in most services (except some container and serverless workloads).

Amazon assigns IPv6 CIDR blocks from their public pool. You don't get to choose your prefix—AWS allocates a /56 block to your VPC from the 2600::/12 range.

Unlike IPv4 where you use private RFC1918 addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), all AWS IPv6 addresses are globally unique and routable (GUA). There's no NAT for IPv6 in the traditional sense—if you want outbound-only connectivity, you use an egress-only internet gateway.


VPC IPv6 Configuration#

Start with your VPC. Adding IPv6 doesn't disrupt existing IPv4 resources.

Associate IPv6 CIDR Block#

Add an Amazon-provided IPv6 CIDR to your VPC:

aws ec2 associate-vpc-cidr-block \
  --vpc-id vpc-0abc123def456 \
  --amazon-provided-ipv6-cidr-block

This command requests an IPv6 allocation from AWS. AWS assigns a /56 block. The response shows your assigned range:

{
  "Ipv6CidrBlockAssociation": {
    "Ipv6CidrBlock": "2600:1f13:1234:5600::/56",
    "Ipv6CidrBlockState": {
      "State": "associating"
    }
  }
}

Wait for the state to change to associated:

aws ec2 describe-vpcs --vpc-ids vpc-0abc123def456 \
  --query 'Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlockState.State'

You can also use "bring your own IP" (BYOIP) if you have provider-independent IPv6 space and want to maintain addresses across AWS accounts or migrations. Most users stick with Amazon-provided addresses.

Subnet IPv6 Configuration#

Subnets need their own /64 slices from the VPC's /56 block.

List your VPC's IPv6 CIDR:

aws ec2 describe-vpcs --vpc-ids vpc-0abc123def456 \
  --query 'Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock'

Output: 2600:1f13:1234:5600::/56

Assign /64 blocks to subnets. Use hex values 00-FF for the subnet portion:

# Public subnet A - 2600:1f13:1234:5600::/64
aws ec2 associate-subnet-cidr-block \
  --subnet-id subnet-0abc111 \
  --ipv6-cidr-block 2600:1f13:1234:5600::/64

This command assigns the first /64 from your VPC's /56 allocation.

# Public subnet B - 2600:1f13:1234:5601::/64
aws ec2 associate-subnet-cidr-block \
  --subnet-id subnet-0abc222 \
  --ipv6-cidr-block 2600:1f13:1234:5601::/64
 
# Private subnet A - 2600:1f13:1234:5610::/64
aws ec2 associate-subnet-cidr-block \
  --subnet-id subnet-0abc333 \
  --ipv6-cidr-block 2600:1f13:1234:5610::/64

Enable auto-assign IPv6 addresses for instances launched in public subnets:

aws ec2 modify-subnet-attribute \
  --subnet-id subnet-0abc111 \
  --assign-ipv6-address-on-creation

Route Tables#

IPv6 routing is separate from IPv4. Create routes for your IPv6 CIDR blocks.

For public subnets, route all IPv6 traffic (::/0) to the internet gateway:

aws ec2 create-route \
  --route-table-id rtb-0abc123 \
  --destination-ipv6-cidr-block ::/0 \
  --gateway-id igw-0def456

Internet gateways handle both IPv4 and IPv6 without modification.

For private subnets, you'll use an egress-only internet gateway (explained later):

aws ec2 create-route \
  --route-table-id rtb-0abc789 \
  --destination-ipv6-cidr-block ::/0 \
  --egress-only-internet-gateway-id eigw-0ghi012

Verify routes:

aws ec2 describe-route-tables --route-table-ids rtb-0abc123 \
  --query 'RouteTables[0].Routes'

EC2 Instance IPv6 Configuration#

EC2 instances need explicit IPv6 address assignment.

Launch New Instances with IPv6#

Specify --ipv6-address-count when launching:

aws ec2 run-instances \
  --image-id ami-0abc123 \
  --instance-type t3.medium \
  --subnet-id subnet-0abc111 \
  --ipv6-address-count 1 \
  --key-name mykey \
  --security-group-ids sg-0abc456

This launches an instance with one IPv6 address automatically assigned.

Or assign a specific IPv6 address from your subnet range:

aws ec2 run-instances \
  --image-id ami-0abc123 \
  --instance-type t3.medium \
  --subnet-id subnet-0abc111 \
  --ipv6-addresses Ipv6Address=2600:1f13:1234:5600::a \
  --key-name mykey \
  --security-group-ids sg-0abc456

Add IPv6 to Existing Instances#

Get the network interface ID (ENI):

aws ec2 describe-instances --instance-ids i-0abc123 \
  --query 'Reservations[0].Instances[0].NetworkInterfaces[0].NetworkInterfaceId'

Assign an IPv6 address:

aws ec2 assign-ipv6-addresses \
  --network-interface-id eni-0def456 \
  --ipv6-address-count 1

Or specify exact addresses:

aws ec2 assign-ipv6-addresses \
  --network-interface-id eni-0def456 \
  --ipv6-addresses 2600:1f13:1234:5600::b

Instance OS Configuration#

Most modern AMIs (Amazon Linux 2023, Amazon Linux 2, Ubuntu 20.04+) configure IPv6 automatically via cloud-init and DHCPv6.

SSH to your instance and verify:

ip -6 addr show

You should see your assigned IPv6 address on the eth0 interface:

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001
    inet6 2600:1f13:1234:5600::a/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::4a2:c9ff:fe12:3456/64 scope link
       valid_lft forever preferred_lft forever

Test connectivity:

ping6 google.com

This verifies IPv6 name resolution and routing work.

curl -6 https://ifconfig.co

If IPv6 isn't configured, check cloud-init logs:

sudo cat /var/log/cloud-init.log | grep -i ipv6

Some older AMIs may require manual configuration. Add to /etc/network/interfaces (Debian/Ubuntu) or /etc/sysconfig/network-scripts/ifcfg-eth0 (RHEL/CentOS).


Security Groups#

Security groups need explicit IPv6 rules. IPv4 rules don't apply to IPv6 traffic.

Creating Dual-Stack Security Group Rules#

Allow HTTP/HTTPS from anywhere (both IPv4 and IPv6):

# IPv4
aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123 \
  --ip-permissions IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges='[{CidrIp=0.0.0.0/0}]'

This allows HTTPS from all IPv4 addresses.

# IPv6
aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123 \
  --ip-permissions IpProtocol=tcp,FromPort=443,ToPort=443,Ipv6Ranges='[{CidrIpv6=::/0}]'

Allow SSH from specific IPv6 prefix:

aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123 \
  --ip-permissions IpProtocol=tcp,FromPort=22,ToPort=22,Ipv6Ranges='[{CidrIpv6=2001:db8::/32,Description="Office IPv6"}]'

Allow all ICMPv6 (required for IPv6 operation):

aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123 \
  --ip-permissions IpProtocol=ipv6-icmp,FromPort=-1,ToPort=-1,Ipv6Ranges='[{CidrIpv6=::/0}]'

Critical: Don't Block ICMPv6

ICMPv6 is essential for IPv6 operation. It handles neighbor discovery, PMTUD, and other core functions. Blocking it will break connectivity.

Security Group Best Practices#

  1. Mirror IPv4 rules to IPv6 unless you have specific reasons not to
  2. Always allow ICMPv6 from the same sources as your application traffic
  3. Use prefix lists for managing complex rule sets across both families
  4. Tag security groups to identify dual-stack vs. IPv4-only

Example: Create a managed prefix list for your organization's IPv6 ranges:

aws ec2 create-managed-prefix-list \
  --prefix-list-name org-ipv6-ranges \
  --address-family IPv6 \
  --max-entries 10 \
  --entries Cidr=2001:db8:100::/40,Description=HQ \
           Cidr=2001:db8:200::/40,Description=DataCenter

Reference it in security group rules:

aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123 \
  --ip-permissions IpProtocol=tcp,FromPort=443,ToPort=443,PrefixListIds='[{PrefixListId=pl-0abc123}]'

Elastic Load Balancing#

Application Load Balancers (ALB) and Network Load Balancers (NLB) support dual-stack. Classic Load Balancers do not.

Application Load Balancer Dual-Stack#

Create or modify an ALB to use dual-stack:

aws elbv2 create-load-balancer \
  --name my-alb \
  --subnets subnet-0abc111 subnet-0abc222 \
  --security-groups sg-0abc123 \
  --ip-address-type dualstack

This creates a load balancer accessible via both IPv4 and IPv6.

For existing load balancers:

aws elbv2 set-ip-address-type \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/1234567890abcdef \
  --ip-address-type dualstack

The load balancer gets both A and AAAA DNS records automatically:

dig my-alb-1234567890.us-east-1.elb.amazonaws.com A
dig my-alb-1234567890.us-east-1.elb.amazonaws.com AAAA

Clients can connect via IPv4 or IPv6. The load balancer forwards traffic to targets using their configured address family (typically IPv4 for backend instances).

Network Load Balancer Dual-Stack#

NLB supports dual-stack similarly:

aws elbv2 create-load-balancer \
  --name my-nlb \
  --type network \
  --subnets subnet-0abc111 subnet-0abc222 \
  --ip-address-type dualstack

NLB preserves client IP addresses, so targets see the actual IPv6 source addresses. Ensure your application logs and security groups handle both address families.

Target Groups#

Target groups register instances by their IPv4 addresses (most common) or IPv6. You can't mix address families in a single target group.

Create IPv4 target group:

aws elbv2 create-target-group \
  --name my-targets-ipv4 \
  --protocol HTTP \
  --port 80 \
  --vpc-id vpc-0abc123 \
  --ip-address-type ipv4

Create IPv6 target group:

aws elbv2 create-target-group \
  --name my-targets-ipv6 \
  --protocol HTTP \
  --port 80 \
  --vpc-id vpc-0abc123 \
  --ip-address-type ipv6

Most deployments use IPv4 target groups even with dual-stack load balancers. The load balancer handles protocol translation transparently.


Egress-Only Internet Gateway#

Egress-only internet gateways allow outbound IPv6 connections from private subnets while blocking inbound connections. This is similar to NAT for IPv4, but without address translation—IPv6 addresses remain globally unique.

Create Egress-Only Gateway#

aws ec2 create-egress-only-internet-gateway \
  --vpc-id vpc-0abc123

Note the gateway ID:

{
  "EgressOnlyInternetGateway": {
    "EgressOnlyInternetGatewayId": "eigw-0abc123"
  }
}

Route Private Subnet Traffic#

Add a route in your private subnet route table:

aws ec2 create-route \
  --route-table-id rtb-0abc789 \
  --destination-ipv6-cidr-block ::/0 \
  --egress-only-internet-gateway-id eigw-0abc123

Instances in private subnets can now initiate outbound IPv6 connections but won't accept inbound traffic from the Internet.

Use cases:

  • Database servers that need software updates
  • Application servers accessing external APIs
  • Internal services calling AWS APIs (S3, DynamoDB, etc.) over IPv6

Security Considerations#

Egress-only gateways don't provide anonymity. Your IPv6 address is still visible to external services. It's stateful firewalling, not NAT.

If you need true outbound-only with address masking, you'll need a NAT64 gateway (more complex setup, rarely necessary).


Route 53 DNS#

Route 53 fully supports IPv6 with AAAA records.

Create AAAA Records#

Add an AAAA record for your instance or load balancer:

aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890ABC \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "app.example.com",
        "Type": "AAAA",
        "TTL": 300,
        "ResourceRecords": [{"Value": "2600:1f13:1234:5600::a"}]
      }
    }]
  }'

Alias Records for Load Balancers#

Use alias records for ALB/NLB (better than AAAA records—automatically updated if LB IPs change):

aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890ABC \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "app.example.com",
        "Type": "AAAA",
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "my-alb-1234567890.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

The HostedZoneId in AliasTarget is the canonical hosted zone for ELB in that region (see AWS documentation for the correct value).

Health Checks#

Route 53 health checks support IPv6:

aws route53 create-health-check \
  --health-check-config \
    IPAddress=2600:1f13:1234:5600::a,Port=443,Type=HTTPS,ResourcePath=/health,RequestInterval=30,FailureThreshold=3 \
  --caller-reference $(uuidgen)

Use health checks with failover or weighted routing policies for high availability.


CloudFront IPv6#

CloudFront distributions support IPv6 by default (can't be disabled in most cases).

CloudFront's global edge network uses dual-stack. Clients connecting over IPv6 hit the same edge locations as IPv4 clients.

Verify IPv6 support:

dig d111111abcdef8.cloudfront.net AAAA

CloudFront handles protocol translation. If your origin is IPv4-only, CloudFront still serves content to IPv6 clients.

Origin configuration doesn't need changes:

aws cloudfront create-distribution \
  --distribution-config file://distribution-config.json

Example distribution-config.json (abbreviated):

{
  "Origins": {
    "Items": [{
      "Id": "my-origin",
      "DomainName": "example.com",
      "CustomOriginConfig": {
        "OriginProtocolPolicy": "https-only"
      }
    }]
  },
  "Enabled": true,
  "Comment": "My distribution"
}

CloudFront automatically provisions IPv6 addresses for your distribution. No additional configuration required.


NAT64 for IPv6-Only Workloads#

Some workloads (Lambda, Fargate) can run IPv6-only. If they need to access IPv4-only resources, use NAT64.

AWS doesn't provide managed NAT64. You must run your own NAT64/DNS64 gateway on EC2.

Example using Tayga (NAT64) and BIND (DNS64):

  1. Launch EC2 instance with both IPv4 and IPv6
  2. Install and configure Tayga for NAT64 translation
  3. Install and configure BIND with DNS64 module
  4. Route IPv6-only resources through NAT64 gateway

This is complex and rarely needed. Most deployments run dual-stack instead.


Terraform Example#

Infrastructure as code for dual-stack AWS setup:

# VPC with IPv6
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  assign_generated_ipv6_cidr_block = true
  enable_dns_support   = true
  enable_dns_hostnames = true
 
  tags = {
    Name = "main-vpc"
  }
}
 
# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}
 
# Egress-Only Internet Gateway
resource "aws_egress_only_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}
 
# Public Subnet
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  ipv6_cidr_block         = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 1)
  assign_ipv6_address_on_creation = true
  availability_zone       = "us-east-1a"
 
  tags = {
    Name = "public-subnet"
  }
}
 
# Route Table for Public Subnet
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
 
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
 
  route {
    ipv6_cidr_block = "::/0"
    gateway_id      = aws_internet_gateway.main.id
  }
 
  tags = {
    Name = "public-rt"
  }
}
 
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}
 
# Security Group
resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP/HTTPS"
  vpc_id      = aws_vpc.main.id
 
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
 
  ingress {
    from_port   = -1
    to_port     = -1
    protocol    = "ipv6-icmp"
    ipv6_cidr_blocks = ["::/0"]
  }
 
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}
 
# EC2 Instance with IPv6
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"  # Amazon Linux 2023
  instance_type = "t3.medium"
  subnet_id     = aws_subnet.public.id
  ipv6_address_count = 1
 
  vpc_security_group_ids = [aws_security_group.web.id]
 
  tags = {
    Name = "web-server"
  }
}
 
# Application Load Balancer
resource "aws_lb" "main" {
  name               = "main-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.web.id]
  subnets            = [aws_subnet.public.id]  # Add more subnets for HA
  ip_address_type    = "dualstack"
 
  tags = {
    Name = "main-alb"
  }
}
 
# Output IPv6 CIDR
output "vpc_ipv6_cidr" {
  value = aws_vpc.main.ipv6_cidr_block
}
 
output "instance_ipv6" {
  value = aws_instance.web.ipv6_addresses
}

Apply with:

terraform init
terraform plan
terraform apply

CloudFormation Example#

YAML template for dual-stack VPC:

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Dual-stack VPC with IPv6'
 
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
 
  IPv6CidrBlock:
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      VpcId: !Ref VPC
      AmazonProvidedIpv6CidrBlock: true
 
  PublicSubnet:
    Type: AWS::EC2::Subnet
    DependsOn: IPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      Ipv6CidrBlock: !Select [0, !Cidr [!GetAtt VPC.Ipv6CidrBlock, 256, 64]]
      AssignIpv6AddressOnCreation: true
      AvailabilityZone: !Select [0, !GetAZs '']
 
  InternetGateway:
    Type: AWS::EC2::InternetGateway
 
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
 
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
 
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
 
  PublicRouteIPv6:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationIpv6CidrBlock: ::/0
      GatewayId: !Ref InternetGateway
 
  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable
 
Outputs:
  VPCId:
    Value: !Ref VPC
  IPv6CidrBlock:
    Value: !GetAtt VPC.Ipv6CidrBlock

Deploy with:

aws cloudformation create-stack \
  --stack-name ipv6-vpc \
  --template-body file://template.yaml

Testing Your AWS IPv6 Deployment#

Verify end-to-end connectivity:

# Test DNS resolution
dig app.example.com AAAA

This verifies your AAAA records are properly configured.

# Test HTTP/HTTPS access
curl -6 https://app.example.com

Test from instance:

ssh ec2-user@<instance-ipv6>
ping6 google.com
curl -6 https://ifconfig.co

Use AWS Reachability Analyzer to verify network paths:

aws ec2 create-network-insights-path \
  --source <instance-eni> \
  --destination <alb-eni> \
  --protocol tcp \
  --destination-port 443
 
aws ec2 start-network-insights-analysis \
  --network-insights-path-id <path-id>

Check for misconfigurations that might block IPv6.


Common Issues and Solutions#

Problem: Instance has IPv6 address but can't reach Internet Solution: Check route table has ::/0 route to IGW, verify security group allows outbound IPv6

Problem: Load balancer doesn't have AAAA record Solution: Ensure ip-address-type is set to dualstack, verify subnets have IPv6 CIDR blocks

Problem: Can't SSH to instance via IPv6 Solution: Add security group rule allowing TCP port 22 from ::/0 or specific IPv6 CIDR

Problem: Egress-only gateway not working Solution: Verify route table association, check instance has IPv6 address assigned

Problem: CloudFront not serving over IPv6 Solution: CloudFront IPv6 is automatic and can't be disabled—check DNS resolution and client connectivity


Cost Considerations#

IPv6 itself is free on AWS. You don't pay for IPv6 CIDR blocks or addresses.

Data transfer costs are the same for IPv4 and IPv6. There's no pricing advantage to IPv6—the benefit is architectural simplicity and avoiding IPv4 exhaustion.

Egress-only internet gateways are free (unlike NAT gateways which cost ~$0.045/hour plus data processing charges).

Verify AWS IPv6 Connectivity

Use our Ping Tool to test your AWS resources, and DNS Tool to verify your Route 53 AAAA records.

Additional Resources#