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:
-
Create an AWS account, download aws-cli and login to it
Create a Virtual Network
-
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
-
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
-
Install easy-rsa, or simply run the source bash script:
brew install easyrsa
-
Create a local CA:
easyrsa init-pki easyrsa --batch --req-cn=Personal-VPN build-ca nopass
-
Create an upstream server certificate:
easyrsa --batch --san=DNS:vpn-server build-server-full personal-vpn-server nopass
-
Create a client certificate:
easyrsa --batch build-client-full personal-vpn-client nopass
-
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
-
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
-
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>
-
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
-
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
-
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
-
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:
- The management of the local CA above isn’t super secure but in most cases it’s sufficient for personal use. You can remove batching and add passphrases to the easyrsa commands, or consider using an ACM CA (which is very expensive) if you have much stronger requirements for securely managing certificates.
- The upstream server certificate will expire in about 2 years by default. I wouldn’t bother trying to update the cert in ACM it’s easier just to tear down the VPN resources and build another one, and you can re-use the VPCs and subnets indefinitely. Same story if you lose the client certificate.
- The above steps should simply be repeated for whichever regions you’d like to connect through. It’s much simpler and faster to create multiple VPNs instead of trying to attach different regional VPCs to a single VPN endpoint.
- This can be a good pattern for accessing personal AWS resources that you don’t want to expose to the internet, such as S3 backups. You can also use a split-tunnel so that only resources which resolve within the VPC will go through the VPN to minimize bandwidth usage and latency.