Published: Jan 4, 2025

Self-Hosting a VPN

Okay, “self-hosting” is doing a bit of heavy lifting here but using your own AWS account is still vastly preferrable to trusting a sketchy VPN provider with your traffic. It’s pretty straightforward and inexpensive to setup a VPN for personal use in AWS, here are all of the steps:

  1. Create an AWS account, download aws-cli and login to it

    Create a Virtual Network

  2. Create a VPC with at least two subnets in the --region and --availability-zones you’d like your traffic to flow to and from (which is useful to circumvent geo-blocking):

    aws ec2 create-vpc --region us-east-1 --cidr-block 10.0.0.0/16 --query Vpc.VpcId --output text
    
    aws ec2 create-subnet --region us-east-1 --availability-zone us-east-1a --vpc-id <vpc-id> --cidr-block 10.0.1.0/24 --query Subnet.SubnetId --output text
    aws ec2 create-subnet --region us-east-1 --availability-zone us-east-1b --vpc-id <vpc-id> --cidr-block 10.0.2.0/24 --query Subnet.SubnetId --output text
    
  3. Create an internet gateway and enable routing through both subnets in the VPC:

    aws ec2 create-internet-gateway --region us-east-1 --query InternetGateway.InternetGatewayId --output text
    aws ec2 attach-internet-gateway --region us-east-1 --vpc-id <vpc-id> --internet-gateway-id <igw-id>
    
    aws ec2 create-route-table --region us-east-1 --vpc-id <vpc-id> --query RouteTable.RouteTableId --output text
    aws ec2 create-route --region us-east-1 --route-table-id <rtb-id> --destination-cidr-block 0.0.0.0/0 --gateway-id <igw-id>
    aws ec2 associate-route-table --region us-east-1 --route-table-id <rtb-id> --subnet-id <subnet-id-a>
    aws ec2 associate-route-table --region us-east-1 --route-table-id <rtb-id> --subnet-id <subnet-id-b>
    

    Create Mutual TLS Certificates

  4. Install easy-rsa, or simply run the source bash script:

    brew install easyrsa
    
  5. Create a local CA:

    easyrsa init-pki
    easyrsa --batch --req-cn=Personal-VPN build-ca nopass
    
  6. Create an upstream server certificate:

    easyrsa --batch --san=DNS:vpn-server build-server-full personal-vpn-server nopass
    
  7. Create a client certificate:

    easyrsa --batch build-client-full personal-vpn-client nopass
    
  8. Upload the server certificate to ACM:

    aws acm import-certificate --region us-east-1 --certificate fileb:///usr/local/etc/pki/issued/personal-vpn-server.crt --private-key fileb:///usr/local/etc/pki/private/personal-vpn-server.key --certificate-chain fileb:///usr/local/etc/pki/ca.crt
    

    Create a Client VPN

  9. Create a client endpoint bound to the server certificate in ACM:

    aws ec2 create-client-vpn-endpoint --region us-east-1 --client-cidr-block 172.0.240.0/20 --server-certificate-arn <server-cert-arn> --authentication-options Type=certificate-authentication,MutualAuthentication={ClientRootCertificateChainArn=<server-cert-arn>} --dns-servers 8.8.8.8 8.8.4.4 --connection-log-options Enabled=false
    
  10. Associate the VPC subnets with the client endpoint (this can take a long time):

    aws ec2 associate-client-vpn-target-network --region us-east-1 --client-vpn-endpoint-id <client-endpoint-id> --subnet-id <subnet-id-a>
    aws ec2 associate-client-vpn-target-network --region us-east-1 --client-vpn-endpoint-id <client-endpoint-id> --subnet-id <subnet-id-b>
    
  11. Ensure that clients can access the VPC and internet gateway:

    aws ec2 authorize-client-vpn-ingress --region us-east-1 --client-vpn-endpoint-id <client-endpoint-id> --target-network-cidr 10.0.0.0/16 --authorize-all-groups
    aws ec2 authorize-client-vpn-ingress --region us-east-1 --client-vpn-endpoint-id <client-endpoint-id> --target-network-cidr 0.0.0.0/0 --authorize-all-groups
    
  12. Ensure that internet traffic routes through the subnets into the public gateway:

    aws ec2 create-client-vpn-route --region us-east-1 --client-vpn-endpoint-id <client-endpoint-id> --destination-cidr-block 0.0.0.0/0 --target-vpc-subnet-id <subnet-id-a>
    aws ec2 create-client-vpn-route --region us-east-1 --client-vpn-endpoint-id <client-endpoint-id> --destination-cidr-block 0.0.0.0/0 --target-vpc-subnet-id <subnet-id-b>
    

    Attach Desktop Client

  13. Dump a client config file:

    aws ec2 export-client-vpn-client-configuration --region us-east-1 --client-vpn-endpoint-id <client-endpoint-id> --output text > ~/personal-vpn.ovpn
    
  14. Configure Viscosity (paid) or alternatively the AWS Client (free)

    Import Connection > From File > ~/personal-vpn.ovpn
    Edit > Authentication > SSL/TLS > Cert > /usr/local/etc/pki/issued/personal-vpn-client.crt
    Edit > Authentication > SSL/TLS > Key > /usr/local/etc/pki/private/personal-vpn-client.key
    

Conclusion

This is a decent setup for personal usage, and should be much less expensive than other hosted options. It’s also a good option if you simply don’t trust hosted VPN providers, which I don’t. Here are a few more things you should consider: