IPv6 OpenVPN Server Setup on AWS EC2 using Rocky Linux

We will be setting up an OpenVPN server with dual-stack IPv6 support using Rocky Linux 9 on AWS.

Problem: Overcoming Lack of Native IPv6 Support from ISP

My current ISP only provides IPv4 connectivity, without access to IPv6 services. The goal here is to be able to access IPv6 websites using VPN, without relying on the ISP to provide IPv6.

Solution

Set up a Rocky Linux 9 OpenVPN server to accept connections over IPv4 and then provide the client with two routes:

  1. IPv4 traffic handled as normal through the VPN.
  2. IPv6 traffic routed via an AWS egress-only IPv6 gateway, giving the client a public IPv6 exit IP.

Pre-requisites

AWS account with admin rights.

Packages awscli and jq on a Linux client.

AWS Setup

1 Credentials

Set up your AWS CLI credentials ~/.aws/credentials on the client machine and then export environment variables to match your setup, e.g.:

export AWS_REGION="eu-west-2"
export AWS_PROFILE="01234567890_AdministratorAccess"

2 Create VPC with IPv4 CIDR

Change CIDR if required (e.g. in case of an overlap).

export VPC_CIDR="10.90.90.0/24"
aws ec2 create-vpc --cidr-block "${VPC_CIDR}" \
  --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=dual-stack-ipv6}]' \
  --query 'Vpc.VpcId' --output text

3 Assign IPv6 CIDR block to VPC

aws ec2 associate-vpc-cidr-block \
  --vpc-id $(aws ec2 describe-vpcs \
    --filters Name=tag:Name,Values=dual-stack-ipv6 \
    --query 'Vpcs[0].VpcId' --output text) \
  --amazon-provided-ipv6-cidr-block

4 Create subnet

aws ec2 create-subnet \
  --vpc-id $(aws ec2 describe-vpcs \
    --filters Name=tag:Name,Values=dual-stack-ipv6 \
    --query 'Vpcs[0].VpcId' --output text) \
  --cidr-block "${VPC_CIDR}" \
  --availability-zone eu-west-2a \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=dual-stack-ipv6-subnet}]' \
  --query 'Subnet.SubnetId' --output text

5 Associate IPv6 /64 to subnet

aws ec2 associate-subnet-cidr-block \
  --subnet-id $(aws ec2 describe-subnets \
    --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \
    --query 'Subnets[0].SubnetId' --output text) \
  --ipv6-cidr-block $(aws ec2 describe-vpcs \
    --filters Name=tag:Name,Values=dual-stack-ipv6 \
    --query 'Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock' \
    --output text | sed 's|/56|/64|')

6 Create Internet Gateway (IGW)

aws ec2 create-internet-gateway \
  --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=dual-stack-ipv6-igw}]' \
  --query 'InternetGateway.InternetGatewayId' --output text

7 Attach IGW to VPC

aws ec2 attach-internet-gateway \
  --internet-gateway-id $(aws ec2 describe-internet-gateways \
    --filters Name=tag:Name,Values=dual-stack-ipv6-igw \
    --query 'InternetGateways[0].InternetGatewayId' --output text) \
  --vpc-id $(aws ec2 describe-vpcs \
    --filters Name=tag:Name,Values=dual-stack-ipv6 \
    --query 'Vpcs[0].VpcId' --output text)

8 Create route table

aws ec2 create-route-table \
  --vpc-id $(aws ec2 describe-vpcs \
    --filters Name=tag:Name,Values=dual-stack-ipv6 \
    --query 'Vpcs[0].VpcId' --output text) \
  --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=dual-stack-ipv6-rt}]' \
  --query 'RouteTable.RouteTableId' --output text

9 Associate route table with subnet

aws ec2 associate-route-table \
  --route-table-id $(aws ec2 describe-route-tables \
    --filters Name=tag:Name,Values=dual-stack-ipv6-rt \
    --query 'RouteTables[0].RouteTableId' --output text) \
  --subnet-id $(aws ec2 describe-subnets \
    --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \
    --query 'Subnets[0].SubnetId' --output text)

10 Add default IPv4 route to IGW

aws ec2 create-route \
  --route-table-id $(aws ec2 describe-route-tables \
    --filters Name=tag:Name,Values=dual-stack-ipv6-rt \
    --query 'RouteTables[0].RouteTableId' --output text) \
  --destination-cidr-block 0.0.0.0/0 \
  --gateway-id $(aws ec2 describe-internet-gateways \
    --filters Name=tag:Name,Values=dual-stack-ipv6-igw \
    --query 'InternetGateways[0].InternetGatewayId' --output text)

11 Create an Egress-Only Internet Gateway (EIGW)

aws ec2 create-egress-only-internet-gateway \
  --vpc-id $(aws ec2 describe-vpcs \
    --filters Name=tag:Name,Values=dual-stack-ipv6 \
    --query 'Vpcs[0].VpcId' --output text) \
  --tag-specifications 'ResourceType=egress-only-internet-gateway,Tags=[{Key=Name,Value=dual-stack-ipv6-eigw}]' \
  --query 'EgressOnlyInternetGateway.EgressOnlyInternetGatewayId' --output text

12 Add default IPv6 route to EIGW (egress-only for IPv6)

aws ec2 create-route \
  --route-table-id $(aws ec2 describe-route-tables \
    --filters Name=tag:Name,Values=dual-stack-ipv6-rt \
    --query 'RouteTables[0].RouteTableId' --output text) \
  --destination-ipv6-cidr-block ::/0 \
  --egress-only-internet-gateway-id $(aws ec2 describe-egress-only-internet-gateways \
    --filters Name=tag:Name,Values=dual-stack-ipv6-eigw \
    --query 'EgressOnlyInternetGateways[0].EgressOnlyInternetGatewayId' --output text)

13 Create security group

aws ec2 create-security-group \
  --group-name dual-stack-ipv6-sg \
  --description "dual-stack-ipv6-sg" \
  --vpc-id $(aws ec2 describe-vpcs --filters Name=tag:Name,Values=dual-stack-ipv6 --query 'Vpcs[0].VpcId' --output text) \
  --tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=dual-stack-ipv6-sg}]' \
  --query 'GroupId' --output text

14 Ingress: SSH (IPv4 from ISP IP)

MY_IP=$(curl -s https://ip.lisenet.com)/32

aws ec2 authorize-security-group-ingress \
  --group-id $(aws ec2 describe-security-groups --filters Name=group-name,Values=dual-stack-ipv6-sg --query 'SecurityGroups[0].GroupId' --output text) \
  --protocol tcp --port 22 --cidr "${MY_IP}"

14 Ingress: OpenVPN (IPv4 from anywhere)

aws ec2 authorize-security-group-ingress \
  --group-id $(aws ec2 describe-security-groups --filters Name=group-name,Values=dual-stack-ipv6-sg --query 'SecurityGroups[0].GroupId' --output text) \
  --protocol udp --port 1194 --cidr "0.0.0.0/0"

15 Enable auto-assign public IPv4 + IPv6 on the subnet

aws ec2 modify-subnet-attribute \
  --subnet-id $(aws ec2 describe-subnets \
    --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \
    --query 'Subnets[0].SubnetId' --output text) \
  --map-public-ip-on-launch
aws ec2 modify-subnet-attribute \
  --subnet-id $(aws ec2 describe-subnets \
    --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \
    --query 'Subnets[0].SubnetId' --output text) \
  --assign-ipv6-address-on-creation

16 Create a key pair (for SSH login)

aws ec2 create-key-pair \
  --key-name dual-stack-ipv6-key \
  --query 'KeyMaterial' --output text > ~/dual-stack-ipv6-key.pem
chmod 0600 ~/dual-stack-ipv6-key.pem

17 Launch Rocky Linux EC2 instance

Please change AMI ID depending on your chosen region.

aws ec2 run-instances \
  --image-id ami-0cfb9357729731907 \
  --instance-type t3a.small \
  --key-name dual-stack-ipv6-key \
  --security-group-ids $(aws ec2 describe-security-groups \
    --filters Name=group-name,Values=dual-stack-ipv6-sg \
    --query 'SecurityGroups[0].GroupId' --output text) \
  --subnet-id $(aws ec2 describe-subnets \
    --filters Name=tag:Name,Values=dual-stack-ipv6-subnet \
    --query 'Subnets[0].SubnetId' --output text) \
  --ipv6-address-count 1 \
  --enable-primary-ipv6 \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=dual-stack-ipv6-ec2}]' \
  --query 'Instances[0].InstanceId' --output text

18 Allocate Elastic IP (EIP) for EC2 instance

aws ec2 allocate-address \
  --domain vpc \
  --tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=Name,Value=dual-stack-ipv6-eip}]' \
  --query 'AllocationId' --output text

19 Associate EIP with EC2 instance

aws ec2 associate-address \
  --instance-id $(aws ec2 describe-instances \
    --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \
    --query 'Reservations[0].Instances[0].InstanceId' --output text) \
  --allocation-id $(aws ec2 describe-addresses \
    --filters Name=tag:Name,Values=dual-stack-ipv6-eip \
    --query 'Addresses[0].AllocationId' --output text)

20 Retrieve public IPv4 address of EC2 instance

aws ec2 describe-instances \
  --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \
  --query 'Reservations[0].Instances[0].NetworkInterfaces[0].Association.PublicIp'

21 SSH into the EC2 instance

ssh -i ~/dual-stack-ipv6-key.pem \
rocky@$(aws ec2 describe-instances \
  --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \
  --query 'Reservations[0].Instances[0].NetworkInterfaces[0].Association.PublicIp'|jq -r)

OpenVPN Server Setup

Run these commands on the VPN server when connected over SSH.

1 Install OpenVPN packages

sudo yum install epel-release
sudo yum install openvpn pkcs11-helper easy-rsa

2 Configure sysctl

Add the following to /etc/sysctl.d/99-sysctl.conf:

net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.eth0.accept_ra=2
net.ipv6.conf.eth0.autoconf=1
net.ipv6.conf.all.disable_ipv6=0
net.ipv6.conf.default.disable_ipv6=0
net.ipv6.conf.lo.disable_ipv6=0

Read values from updated file:

sudo sysctl -p

3 Create cloud-init patch

Create a new file /etc/cloud/cloud.cfg.d/99_ipv6_dualstack.cfg and add the following lines:

runcmd:
  - |
    CON=$(nmcli -t -f NAME,DEVICE con show --active | awk -F: '$2=="eth0"{print $1; exit}')
    [ -z "$CON" ] && CON=$(nmcli -t -f NAME con show | head -n1)
    nmcli con modify "$CON" ipv4.method auto
    nmcli con modify "$CON" ipv6.method auto
    nmcli con up "$CON" || nmcli dev reapply eth0

4 Configure firewall (iptables)

Install iptables:

sudo yum install iptables-services iptables
sudo systemctl enable --now iptables ip6tables

Delete all existing rules:

sudo iptables -F
sudo ip6tables -F

Create new rules for IPv4:

sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A INPUT -p icmp -j ACCEPT
sudo iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p udp -m state --state NEW -m udp --dport 1194 -j ACCEPT
sudo iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
sudo iptables -t nat -A POSTROUTING -s 10.8.8.0/24 -j MASQUERADE

Create new rules for IPv6:

sudo ip6tables -A INPUT -i lo -j ACCEPT
sudo ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo ip6tables -A INPUT -p ipv6-icmp -j ACCEPT
sudo ip6tables -A INPUT -d fe80::/64 -p udp -m udp --dport 546 -m state --state NEW -j ACCEPT
sudo ip6tables -A INPUT -j REJECT --reject-with icmp6-adm-prohibited
sudo ip6tables -A FORWARD -s fd00:10:8:8::/64 -i tun0 -o eth0 -j ACCEPT
sudo ip6tables -A FORWARD -d fd00:10:8:8::/64 -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo ip6tables -t nat -A POSTROUTING -s fd00:10:8:8::/64 -o eth0 -j MASQUERADE

Save the rules:

sudo service iptables save
sudo service ip6tables save

5 Configure OpenVPN

cd /etc/openvpn/
sudo /usr/share/easy-rsa/3/easyrsa --batch init-pki
sudo /usr/share/easy-rsa/3/easyrsa --batch build-ca nopass
sudo /usr/share/easy-rsa/3/easyrsa --batch --days=365 build-server-full server nopass
sudo /usr/share/easy-rsa/3/easyrsa --batch --days=365 build-client-full client nopass
sudo /usr/share/easy-rsa/3/easyrsa --batch gen-dh
sudo /usr/share/easy-rsa/3/easyrsa --batch --days=365 gen-crl
sudo openvpn --genkey secret ./pki/ta.key

6 Create OpenVPN server config file

Create a new file /etc/openvpn/server/server.conf and add the following lines:

proto udp
port 1194
dev tun
server 10.8.8.0 255.255.255.0
# A unique local address (ULA) is in the block fc00::/7.
server-ipv6 fd00:10:8:8::/64
topology subnet
persist-key
persist-tun
keepalive 10 60
max-clients 253
cipher AES-256-GCM
comp-lzo no
reneg-sec 0

ca /etc/openvpn/pki/ca.crt
cert /etc/openvpn/pki/issued/server.crt
key /etc/openvpn/pki/private/server.key
dh /etc/openvpn/pki/dh.pem

tls-auth /etc/openvpn/pki/ta.key 0

auth SHA512

user nobody
group nobody
verb 3

explicit-exit-notify 1
status /var/log/openvpn-status.log
log-append /var/log/openvpn.log

# Clients to use VPN as default IPv4 route.
push "redirect-gateway def1 bypass-dhcp"

# Give the client a default IPv6 route via the VPN,
# like redirect-gateway def1 does for IPv4.
push "route-ipv6 2000::/3"

# IPv6 DNS for clients.
push "dhcp-option DNS6 2001:4860:4860::8888"
push "dhcp-option DNS6 2001:4860:4860::8844"

7 Enable and start OpenVPN server service

sudo systemctl enable --now openvpn-server@server

Check tun0 interface:

ip ad show dev tun0
3: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none 
    inet 10.8.8.1/24 scope global tun0
       valid_lft forever preferred_lft forever
    inet6 fd00:10:8:8::1/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::1135:b02f:161b:c6d/64 scope link stable-privacy 
       valid_lft forever preferred_lft forever

Check ping:

ping -4 -c3 google.com
PING  (142.251.29.139) 56(84) bytes of data.
64 bytes from uv-in-f139.1e100.net (142.251.29.139): icmp_seq=1 ttl=115 time=1.11 ms
64 bytes from uv-in-f139.1e100.net (142.251.29.139): icmp_seq=2 ttl=115 time=1.14 ms
64 bytes from uv-in-f139.1e100.net (142.251.29.139): icmp_seq=3 ttl=115 time=1.14 ms

---  ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.114/1.131/1.140/0.012 ms
ping -6 -c3 google.com
PING google.com(wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b)) 56 data bytes
64 bytes from wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b): icmp_seq=1 ttl=114 time=1.49 ms
64 bytes from wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b): icmp_seq=2 ttl=114 time=1.53 ms
64 bytes from wj-in-f139.1e100.net (2a00:1450:4009:c0b::8b): icmp_seq=3 ttl=114 time=1.52 ms

--- google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.494/1.516/1.530/0.015 ms

8 Create OpenVPN client config file

On the VPN server: copy client certificates to the rocky user home directory.

sudo cp /etc/openvpn/pki/ca.crt /home/rocky/
sudo cp /etc/openvpn/pki/ta.key /home/rocky/
sudo cp /etc/openvpn/pki/private/client.key /home/rocky/
sudo cp /etc/openvpn/pki/issued/client.crt /home/rocky/
sudo chown rocky: /home/rocky/{*crt,*key}

On the VPN client: install openvpn package on the client machine (choose your package manager).

[client]$ sudo apt install openvpn
[client]$ sudo yum install openvpn

Copy client certificates from the VPN server to the VPN client machine:

[client]$ scp -i ~/dual-stack-ipv6-key.pem \
rocky@$(aws ec2 describe-instances \
  --filters Name=tag:Name,Values=dual-stack-ipv6-ec2 \
  --query 'Reservations[0].Instances[0].NetworkInterfaces[0].Association.PublicIp'|jq -r):~/{*crt,*key} ~/

Move copied certificates to /etc/openvpn/client/ directory:

[client]$ sudo mv ~/{*crt,*key} /etc/openvpn/client/

Create an OpenVPN client config file client-dual-stack-ipv6.ovpn and add the following lines:

client
remote REPLACE_WITH_VPN_SERVER_EIP
proto udp
port 1194
dev tun
nobind
comp-lzo no
reneg-sec 0
remote-cert-tls server
key-direction 1
auth SHA512
cipher AES-256-GCM
verb 3

ca /etc/openvpn/client/ca.crt
cert /etc/openvpn/client/client.crt
key /etc/openvpn/client/client.key
tls-auth /etc/openvpn/client/ta.key 1

9 Connect OpenVPN client to the server

[client]$ sudo openvpn --config client-dual-stack-ipv6.ovpn

Check tun0 interface:

[client]$ ip ad show dev tun0
132: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none 
    inet 10.8.8.2/24 scope global tun0
       valid_lft forever preferred_lft forever
    inet6 fd00:10:8:8::1000/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::b3c2:ab0d:2878:36f8/64 scope link stable-privacy 
       valid_lft forever preferred_lft forever

Check ping:

[client]$ ping -4 -c3 google.com
PING google.com (142.250.187.206) 56(84) bytes of data.
64 bytes from lhr25s33-in-f14.1e100.net (142.250.187.206): icmp_seq=1 ttl=114 time=14.6 ms
64 bytes from lhr25s33-in-f14.1e100.net (142.250.187.206): icmp_seq=2 ttl=114 time=14.6 ms
64 bytes from lhr25s33-in-f14.1e100.net (142.250.187.206): icmp_seq=3 ttl=114 time=15.0 ms

--- google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 14.569/14.737/15.020/0.201 ms
[client]$ ping -6 -c3 google.com
PING google.com (2a00:1450:4009:c04::64) 56 data bytes
64 bytes from um-in-f100.1e100.net (2a00:1450:4009:c04::64): icmp_seq=1 ttl=111 time=15.7 ms
64 bytes from um-in-f100.1e100.net (2a00:1450:4009:c04::64): icmp_seq=2 ttl=111 time=14.7 ms
64 bytes from um-in-f100.1e100.net (2a00:1450:4009:c04::64): icmp_seq=3 ttl=111 time=14.4 ms

--- google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 14.376/14.933/15.682/0.550 ms

You should now be able to access IPv6 websites on the internet.

Leave a Reply

Your email address will not be published. Required fields are marked *