Using Cloudflare WARP for IPv4 communication from an IPv6-only EC2 instance

Using Cloudflare WARP for IPv4 communication from an IPv6-only EC2 instance

I conducted verification on EC2 (Amazon Linux 2023) in a VPC where internet communication paths were limited to only IPv6 (Egress-Only Gateway). By using Cloudflare WARP for IPv4 over IPv6 tunneling, I was able to secure an IPv4 internet communication path with zero fixed costs. I also confirmed that SSM could be used.
2026.01.10

This page has been translated by machine translation. View original

Since February 1, 2024, AWS has begun charging for Public IPv4 addresses. This fee is $0.005/hour, which translates to approximately $3.6/address per month (30 days), a cost comparable to the monthly fee for a t4g.nano instance (approximately $3.94).

Meanwhile, as of January 2025, some AWS services like SSM (Systems Manager) are still only available via IPv4. To use these services from EC2 instances without public IPv4 addresses, NAT Gateways or VPC endpoints were previously required.

In this article, I'll introduce how I tested whether IPv4 communication is possible through Cloudflare WARP's tunnel (IPv4 over IPv6) without using public IPv4 addresses or NAT Gateways.

Architecture Overview

The components of the environment we'll build are as follows:

  • VPC: IPv4/IPv6 dual-stack (but IPv4 is private only, IGW is omitted)
  • Egress-Only Internet Gateway: IPv6 outbound only
  • S3 Gateway Endpoint: Used as a route for package acquisition from the official Amazon Linux 2023 repository and for S3 connection to temporarily store the WARP RPM
  • EC2 Instance Connect Endpoint: Used as an SSH connection route from outside in an Egress-Only Gateway environment
  • Cloudflare WARP: Implementing IPv4 over IPv6

Why This Configuration Works

How Cloudflare WARP Works

Cloudflare WARP is a WireGuard-based VPN tunnel. It connects to Cloudflare via IPv6, and Cloudflare performs the conversion to IPv4, enabling IPv4 over IPv6.

Environment Setup

VPC Setup (vpc-template.yaml)

I created a VPC with the following resources using CloudFormation:

  • VPC (IPv4/IPv6 dual-stack)
  • Two private subnets
  • Egress-Only Internet Gateway
  • S3 Gateway Endpoint
  • EC2 Instance Connect Endpoint
Full template
AWSTemplateFormatVersion: '2010-09-09'
Description: 'IPv6-only VPC Test - Complete private VPC with IPv6 connectivity'

Parameters:
  ProjectName:
    Type: String
    Default: 'ipv6-vpc-test'
    Description: 'IPv6-only VPC Test - Project name prefix for AWS resources'

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: '192.168.0.0/18'
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Ref ProjectName

  # IPv6 CIDR Block
  IPv6CidrBlock:
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      VpcId: !Ref VPC
      AmazonProvidedIpv6CidrBlock: true

  # Private Subnets
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    DependsOn: IPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '192.168.0.0/20'
      AvailabilityZone: !Select [0, !GetAZs '']
      Ipv6CidrBlock: !Select [0, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 2, 64]]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-private-subnet-1a'

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    DependsOn: IPv6CidrBlock
    Properties:
      VpcId: !Ref VPC
      CidrBlock: '192.168.16.0/20'
      AvailabilityZone: !Select [1, !GetAZs '']
      Ipv6CidrBlock: !Select [1, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 2, 64]]
      AssignIpv6AddressOnCreation: true
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-private-subnet-1c'

  # Egress-Only Internet Gateway (IPv6)
  EgressOnlyInternetGateway:
    Type: AWS::EC2::EgressOnlyInternetGateway
    Properties:
      VpcId: !Ref VPC

  # Private Route Table
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-private-rt'

  # IPv6 Route (via Egress-Only IGW)
  PrivateRouteIPv6:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationIpv6CidrBlock: '::/0'
      EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway

  # Subnet Associations
  PrivateSubnet1Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2Association:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  # VPC Endpoint - S3
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref PrivateRouteTable

  # EC2 Instance Connect Endpoint
  EC2InstanceConnectEndpoint:
    Type: AWS::EC2::InstanceConnectEndpoint
    Properties:
      SubnetId: !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref EC2InstanceConnectSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-eice'

  # Security Group for EC2 Instance Connect Endpoint
  EC2InstanceConnectSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for EC2 Instance Connect Endpoint
      VpcId: !Ref VPC
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 192.168.0.0/18
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-eice-sg'

Outputs:
  VPCId:
    Description: 'VPC ID'
    Value: !Ref VPC
    Export:
      Name: !Sub '${ProjectName}-vpc-id'

  PrivateSubnet1Id:
    Description: 'Private Subnet 1 ID'
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub '${ProjectName}-private-subnet-1-id'

  PrivateSubnet2Id:
    Description: 'Private Subnet 2 ID'
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub '${ProjectName}-private-subnet-2-id'

Preparing the WARP RPM

Since there is no official Cloudflare WARP repository for Amazon Linux 2023, I used the one for CentOS/RHEL8.
The WARP dependencies (desktop-file-utils nftables nss-tools) were installed from the official Amazon Linux 2023 repository.
For WARP itself, I downloaded the CentOS/RHEL8 RPM from the Cloudflare download page, saved it to S3, and used it for installation.

EC2 Instance Setup (ec2-warp-launch-template.yaml)

I created an EC2 environment with the following resources using CloudFormation:

  • IAM role (for SSM + S3 access)
  • Security group (allowing SSH from the EC2 Instance Connect Endpoint)
  • Launch Template (configuring Cloudflare WARP in UserData)
  • EC2 instance (launched using the Launch Template)
Full template
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Launch Template with Cloudflare WARP in IPv6-only VPC (ARM64 Optimized)'

Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: 'VPC ID'

  SubnetId:
    Type: AWS::EC2::Subnet::Id
    Description: 'Subnet ID'

  InstanceType:
    Type: String
    Default: 't4g.micro'
    AllowedValues: ['t4g.nano', 't4g.micro', 't4g.small']
    Description: 'EC2 instance type (ARM64 only)'

  KeyPairName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: 'EC2 Key Pair for SSH access'

  WarpRpmS3Uri:
    Type: String
    Description: 'S3 URI for WARP RPM'

  ImageId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64'
    Description: 'AMI ID for EC2 instance'

Resources:
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-ec2-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Policies:
        - PolicyName: WarpS3Access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: s3:GetObject
                Resource: arn:aws:s3:::cloudflare-warp-temp/*

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub '${AWS::StackName}-instance-profile'
      Roles:
        - !Ref EC2Role

  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security group for WARP EC2 instance'
      VpcId: !Ref VpcId
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIpv6: '::/0'
          Description: 'All outbound traffic over IPv6'
        - IpProtocol: -1
          CidrIp: '0.0.0.0/0'
          Description: 'All outbound traffic over IPv4'
      Tags:
        - Key: Name
          Value: 'warp-ec2-sg'

  WarpLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Sub '${AWS::StackName}-launch-template'
      LaunchTemplateData:
        ImageId: !Ref ImageId
        InstanceType: !Ref InstanceType
        KeyName: !Ref KeyPairName
        IamInstanceProfile:
          Arn: !GetAtt EC2InstanceProfile.Arn
        NetworkInterfaces:
          - DeviceIndex: 0
            SubnetId: !Ref SubnetId
            Ipv6AddressCount: 1
            Groups:
              - !Ref EC2SecurityGroup
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash
            # Install dependencies
            dnf install -y desktop-file-utils nftables nss-tools
            # Download and install WARP
            aws s3 cp "${WarpRpmS3Uri}" /tmp/cloudflare-warp.rpm
            rpm -ivh /tmp/cloudflare-warp.rpm
            # Start WARP service
            systemctl enable --now warp-svc
            for i in {1..30}; do warp-cli status &>/dev/null && break; sleep 2; done
            # Register and configure WARP
            script -q -c "yes | warp-cli registration new" /dev/null
            warp-cli mode warp
            # Exclude IPv6 from WARP tunnel (IPv4 only through WARP)
            warp-cli tunnel ip add-range ::/0
            warp-cli connect
            # Restart SSM agent
            systemctl restart amazon-ssm-agent

  WarpEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      LaunchTemplate:
        LaunchTemplateId: !Ref WarpLaunchTemplate
        Version: !GetAtt WarpLaunchTemplate.LatestVersionNumber

Outputs:
  InstanceId:
    Description: 'EC2 Instance ID'
    Value: !Ref WarpEC2Instance
    Export:
      Name: !Sub '${AWS::StackName}-instance-id'

Key Points for WARP Configuration

  • Starting warp-svc can take several tens of seconds, so I added a wait process
# Enable and start WARP service
systemctl enable --now warp-svc

# Wait for service to start (max 60 seconds)
for i in {1..30}; do warp-cli status &>/dev/null && break; sleep 2; done
  • During warp-cli registration new, you're interactively asked to agree to terms, which I handled with a pseudo TTY
# Register WARP
script -q -c "yes | warp-cli registration new" /dev/null
  • I implemented a split tunnel configuration to exclude IPv6 communication from the WARP tunnel
# Exclude IPv6 from WARP tunnel (IPv4 only through WARP)
warp-cli tunnel ip add-range ::/0

For Cloudflare WARP's terms of service and constraints, please check the official documentation and FAQ (https://developers.cloudflare.com/warp-client/known-issues-and-faq/).

Verification

I connected via the EC2 Instance Connect Endpoint using SSH to perform the verification.

$ aws ec2-instance-connect ssh --instance-id i-xxxxxxxxxxxxx71f5 --region ap-northeast-1

WARP Status Check

$ warp-cli status
Status update: Connected
Network: healthy

IPv6 Communication Check

$ curl -6 -s https://ipv6.icanhazip.com
2406:da14:xxxx:xxxx:xxxx:xxxx:848b:afaa

The global IPv6 address assigned to the EC2 was returned. This is going through the Egress-Only IGW.

IPv4 Communication Check

$ curl -4 -s https://ipv4.icanhazip.com
104.xx.xxx.105

An IPv4 address owned by Cloudflare was returned. This confirms that the WARP tunnel is functioning properly.

SSM Connection Check

After configuring WARP, I confirmed that SSM's "start-session" could be used.

$ aws ssm start-session --target i-xxxxxxxxxxxxx71f5 --region ap-northeast-1

I was able to connect using Session Manager even to an EC2 without public IPv4 addresses, NAT Gateway, or VPC endpoints (ssm, ssmmessages, ec2messages).

SSM connection

Cost Comparison

Item EC2 Public IP NAT Gateway Cloudflare WARP
Fixed cost $0.005/hour (approx. $3.6/month) $0.062/hour (approx. $45/month) $0
EC2 data transfer (in) $0 $0 $0
EC2 data transfer (out) $0.114/GB $0.114/GB $0.114/GB
NAT Gateway processing - $0.062/GB -

Conclusion

I've confirmed that even in an IPv6-only VPC environment without NAT Gateway, services requiring IPv4 communication like SSM can be used by using Cloudflare WARP.

For workloads that primarily use IPv6 communication and use IPv4 communication for auxiliary purposes, especially for low-spec instances or development environments leveraging the t4g.small free tier extended to the end of 2026, IPv4 over IPv6 connection via WARP is effective.

However, Amazon Linux 2023 is not included in Cloudflare WARP's list of supported operating systems, and support from both Cloudflare and AWS cannot be expected. I strongly recommend limiting the configuration introduced in this article to testing environments without service impact or personal use.

In the future, I hope that AWS will officially provide a low-cost IPv4 over IPv6 mechanism, and that dual-stack support will advance in services from AWS and other companies, enabling a practical IPv6 native environment.

https://dev.classmethod.jp/articles/access-ipv4-internet-from-ipv6-only-subnet-via-cloudflare-warp/

Share this article

FacebookHatena blogX

Related articles