With the recent Bay Area rolling blackouts by PG&E, we decided to migrate our current VPN from our office to AWS. This should solve two issue that we are facing; one, when our internet goes out at the office; two, as I mentioned, when PG&E decides to cut power randomly! :P
List of requirements that we want from migrating to AWS Client VPN Endpoing:
- Reliability
- Ability to access our internal VPCs
- Resolve private and public DNS entries
- Static NAT IP for whitelisting
- User certificate management
And of course we want to do this via Terraform as much as possible.
- Pricing
- Certificate Creation
- Setup Client VPN Endpoint
- Managing User Certificate
- Removing Certificate
- Auditing
- AWS Client VPN Download
Pricing
So this is NOT free, there is a charge per connection for the full hour. More pricing information here.
Certificate Creation
Clone Easy RSA Git Repo.
$ git clone https://github.com/OpenVPN/easy-rsa.git
Initialize Public Key Infrastructure (PKI).
$ cd easy-rsa/easyrsa3
$ ./easyrsa init-pki
init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /var/folders/xf/st0zj2rn1pl1jtmkjzlnl82c0000gn/T/tmp.hBPssWon/easy-rsa/easyrsa3/pki
Build Certificate Authority.
$ ./easyrsa build-ca nopass
Using SSL: openssl LibreSSL 2.8.3
Generating RSA private key, 2048 bit long modulus
.................................................................+++
...............................+++
e is 65537 (0x10001)
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:aws-vpn.company.com
CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/var/folders/xf/st0zj2rn1pl1jtmkjzlnl82c0000gn/T/tmp.hBPssWon/easy-rsa/easyrsa3/pki/ca.crt
Build Server Certificate.
$ ./easyrsa build-server-full server.aws-vpn.company.com nopass
Using SSL: openssl LibreSSL 2.8.3
Generating a 2048 bit RSA private key
...............................+++
.........+++
writing new private key to '/var/folders/xf/st0zj2rn1pl1jtmkjzlnl82c0000gn/T/tmp.hBPssWon/easy-rsa/easyrsa3/pki/easy-rsa-43480.72nxYw/tmp.WW2DKy'
-----
Using configuration from /var/folders/xf/st0zj2rn1pl1jtmkjzlnl82c0000gn/T/tmp.hBPssWon/easy-rsa/easyrsa3/pki/easy-rsa-43480.72nxYw/tmp.MCadXI
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName :ASN.1 12:'server.aws-vpn.company.com'
Certificate is to be certified until Nov 16 21:10:53 2022 GMT (825 days)
Write out database with 1 new entries
Data Base Updated
Build Client Certificate.
$ ./easyrsa build-client-full client.aws-vpn.company.com nopass
Using SSL: openssl LibreSSL 2.8.3
Generating a 2048 bit RSA private key
...........................................................................................................................................................................................................+++
.................+++
writing new private key to '/var/folders/xf/st0zj2rn1pl1jtmkjzlnl82c0000gn/T/tmp.hBPssWon/easy-rsa/easyrsa3/pki/easy-rsa-41705.PurLiw/tmp.4iQdhO'
-----
Using configuration from /var/folders/xf/st0zj2rn1pl1jtmkjzlnl82c0000gn/T/tmp.hBPssWon/easy-rsa/easyrsa3/pki/easy-rsa-41705.PurLiw/tmp.iNaWh1
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName :ASN.1 12:'client.aws-vpn.company.com'
Certificate is to be certified until Nov 16 21:08:51 2022 GMT (825 days)
Write out database with 1 new entries
Data Base Updated
Verify the certs and keys.
$ find . | egrep ".key|.crt"
./pki/ca.crt
./pki/issued/server.aws-vpn.company.com.crt
./pki/issued/client.aws-vpn.company.com.crt
./pki/private/server.aws-vpn.company.com.key
./pki/private/client.aws-vpn.company.com.key
./pki/private/ca.key
Upload to AWS Certificate Manager (ACM).
$ aws acm import-certificate --region us-east-1 --certificate fileb://pki/issued/server.aws-vpn.company.com.crt --private-key fileb://pki/private/server.aws-vpn.company.com.key --certificate-chain fileb://pki/ca.crt
$ aws acm import-certificate --region us-east-1 --certificate fileb://pki/issued/client.aws-vpn.company.com.crt --private-key fileb://pki/private/client.aws-vpn.company.com.key --certificate-chain fileb://pki/ca.crt
Backup CA cert/key in parameter store. We pull the CA crt/key from parameter store when we generate certificates for users.
Notice! These will be used when creating certificates for individual users.
$ aws ssm put-parameter --region us-east-1 --name /vpn/crt/ca --value "$(cat pki/ca.crt | base64)" --type SecureString
$ aws ssm put-parameter --region us-east-1 --name /vpn/key/ca --value "$(cat pki/private/ca.key | base64)" --type SecureString
Setup Client VPN Endpoint
We will do this via terraform.
The following setting will associate the Client VPN Endpoint to the central VPC’s central-backend subnet with destination of 0.0.0.0/0’s target nat that is associated with central-public (Elastic IP Address).
Module
main.tf
################################
### Security Group
################################
resource aws_security_group "client-vpn-access" {
name = "terraform-shared-client-vpn-access"
vpc_id = var.vpc_id
ingress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
protocol = "-1"
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}
################################
### Cloudwatch Log Group
################################
resource "aws_cloudwatch_log_group" "cloudwatch-log-group" {
name = "client-vpn-endpoint-${var.domain}"
retention_in_days = "30"
tags = {
Name = var.domain
Environment = "global"
Terraform = "true"
}
}
################################
### Client VPN Endpoint
################################
resource "aws_ec2_client_vpn_endpoint" "client-vpn-endpoint" {
description = "terraform-client-vpn-endpoint"
server_certificate_arn = var.server_cert
client_cidr_block = var.client_cidr_block
split_tunnel = var.split_tunnel
dns_servers = var.dns_servers
authentication_options {
type = "certificate-authentication"
root_certificate_chain_arn = var.client_cert
}
connection_log_options {
enabled = true
cloudwatch_log_group = aws_cloudwatch_log_group.cloudwatch-log-group.name
}
tags = {
Name = var.domain
Environment = "global"
Terraform = "true"
}
}
resource "aws_ec2_client_vpn_network_association" "client-vpn-network-association" {
count = length(var.subnet_id)
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.client-vpn-endpoint.id
subnet_id = element(var.subnet_id, count.index)
}
resource "aws_ec2_client_vpn_route" "client-vpn-route" {
count = length(var.subnet_id)
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.client-vpn-endpoint.id
destination_cidr_block = "0.0.0.0/0"
target_vpc_subnet_id = element(var.subnet_id, count.index)
description = "Internet Access"
depends_on = [
aws_ec2_client_vpn_endpoint.client-vpn-endpoint,
aws_ec2_client_vpn_network_association.client-vpn-network-association
]
}
################################
### Null Resource
################################
resource "null_resource" "authorize-client-vpn-ingress" {
provisioner "local-exec" {
when = create
command = "aws --region ${var.region} ec2 authorize-client-vpn-ingress --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.client-vpn-endpoint.id} --target-network-cidr 0.0.0.0/0 --authorize-all-groups"
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_ec2_client_vpn_endpoint.client-vpn-endpoint,
aws_ec2_client_vpn_network_association.client-vpn-network-association
]
}
resource null_resource "client-vpn-security-group" {
provisioner "local-exec" {
when = create
command = "aws ec2 apply-security-groups-to-client-vpn-target-network --client-vpn-endpoint-id ${aws_ec2_client_vpn_endpoint.client-vpn-endpoint.id} --vpc-id ${aws_security_group.client-vpn-access.vpc_id} --security-group-ids ${aws_security_group.client-vpn-access.id}"
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_ec2_client_vpn_endpoint.client-vpn-endpoint,
aws_security_group.client-vpn-access
]
}
variables.tf
variable "region" {
default = "us-east-1"
}
variable "client_cidr_block" {
description = "The IPv4 address range, in CIDR notation being /22 or greater, from which to assign client IP addresses"
default = "18.0.0.0/22"
}
variable "vpc_id" {
description = "The ID of the VPC to associate with the Client VPN endpoint."
}
variable "subnet_id" {
type = list
description = "The ID of the subnet to associate with the Client VPN endpoint."
}
variable "domain" {
default = "aws-vpn.reflektion.com"
}
variable "server_cert" {
description = "Server certificate"
}
variable "client_cert" {
description = "Client/Root certificate"
}
variable "split_tunnel" {
default = "false"
}
variable "dns_servers" {
type = list
default = [
"8.8.8.8",
"1.1.1.1"
]
}
output.tf
output "client_vpn_endpoint_id" {
value = aws_ec2_client_vpn_endpoint.client-vpn-endpoint.id
}
Environment
########################
## Client VPN Endpoint
########################
module "client-vpn" {
source = "../../modules/aws_client_vpn_endpoint"
region = "us-east-1"
client_cidr_block = "10.22.0.0/22" // 1024
vpc_id = "vpc-*****" // central
subnet_id = ["subnet-*****", "subnet-*****"] // central-backend w/ route table 0.0.0.0/0 which has central-public EIPs
domain = "aws-vpn.reflektion.com"
server_cert = "arn:aws:acm:us-east-1:*****:certificate/*****-12e7-4da8-836b-*****"
client_cert = "arn:aws:acm:us-east-1:*****:certificate/*****-12b7-4995-9f1e-*****"
dns_servers = ["10.16.0.2", "8.8.8.8"]
}
Managing User Certificate
Notice! This is sensitive/secure data, DO NOT share with anyone else! as they contain login credentials.
The following script will generate a .ovpn
config file which then can be shared with user securely (via secure gist url).
#!/usr/bin/env bash
# This script generates certificate for client vpn
# AWS Client VPN Pricing: https://aws.amazon.com/vpn/pricing/
[ -z "$1" ] && echo "Usage: $0 first.last" && exit 1
export EASYRSA_BATCH=1
client="$1"
cert_domain="aws-vpn.company.com"
client_full="${client}.${cert_domain}"
outfile="/tmp/${client}-cvpn-endpoint.ovpn"
vpn_endpoint_id="cvpn-endpoint-*****"
tput setaf 2; echo ">> clone repo ..."; tput setaf 9
cd `mktemp -d`
git clone https://github.com/OpenVPN/easy-rsa.git
cd easy-rsa/easyrsa3
tput setaf 2; echo ">> init ..."; tput setaf 9
./easyrsa init-pki
./easyrsa build-ca nopass
rm -f pki/ca.crt
rm -f pki/private/ca.key
tput setaf 2; echo ">> get ca crt/key from param store ..."; tput setaf 9
(aws ssm get-parameters \
--region us-east-1 \
--names /vpn/crt/ca \
--with-decryption \
--query Parameters[0].Value \
--output text || exit 2) | base64 --decode > pki/ca.crt
(aws ssm get-parameters \
--region us-east-1 \
--names /vpn/key/ca \
--with-decryption \
--query Parameters[0].Value \
--output text || exit 3) | base64 --decode > pki/private/ca.key
tput setaf 2; echo ">> create and import crt/key for $client ..."; tput setaf 9
./easyrsa build-client-full ${client_full} nopass
aws acm import-certificate \
--certificate fileb://pki/issued/${client_full}.crt \
--private-key fileb://pki/private/${client_full}.key \
--certificate-chain fileb://pki/ca.crt
tput setaf 2; echo ">> generate vpn client config file ..."; tput setaf 9
aws ec2 export-client-vpn-client-configuration \
--client-vpn-endpoint-id $vpn_endpoint_id \
--output text >| $outfile
sed -i~ "s/^remote /remote ${client}./" $outfile
echo "<cert>" >> $outfile
cat pki/issued/${client_full}.crt >> $outfile
echo "</cert>" >> $outfile
echo "<key>" >> $outfile
cat pki/private/${client_full}.key >> $outfile
echo "</key>" >> $outfile
tput setaf 2; echo ">> $outfile ..."; tput setaf 9
ls -alh $outfile
Removing Certificate
When an employee leaves or no longer need VPN access, we simply just go to AWS console and remove their certificate from ACM.
Auditing
Assuming you have aws-utils cloned, you can use the vpnlookup script in the repo to do some basic auditing.
For our environment, we have to update ~/.aws-utils/vpnlookup.conf
.
$ cat ~/.aws-utils/vpnlookup.conf
cvpn_endpoint_id="cvpn-endpoint-*****"
cvpn_cert_domain_name="aws-vpn.company.com"
Connections
$ vpnlookup conn | column -ts,
cvpn-connection-***** cvpn-endpoint-***** 2020-08-13T21:36:04 2020-08-13T21:36:03 3075 2732 8 7 10.22.1.162 calvin.wong.aws-vpn.company.com terminated 2020-08-13T21:36:04
cvpn-connection-***** cvpn-endpoint-***** 2020-08-13T21:36:12 2020-08-13T21:36:07 3075 2732 8 7 10.22.0.2 calvin.wong.aws-vpn.company.com terminated 2020-08-13T21:36:12
cvpn-connection-***** cvpn-endpoint-***** 2020-08-13T21:36:17 2020-08-13T21:36:17 3075 2732 8 7 10.22.0.34 calvin.wong.aws-vpn.company.com active -
Certificates Matching Our Domain Name
$ vpnlookup cert | column -ts,
client.aws-vpn.company.com arn:aws:acm:us-east-1:*****:certificate/*****-6d38-4b10-8d1c-*****
server.aws-vpn.company.com arn:aws:acm:us-east-1:*****:certificate/*****-c706-4dc5-aa86-*****
erin.ewing.aws-vpn.company.com arn:aws:acm:us-east-1:*****:certificate/*****-615e-4306-916b-*****
calvin.wong.aws-vpn.company.com arn:aws:acm:us-east-1:*****:certificate/*****-adb3-40dc-a93d-*****
dennis.dalton.aws-vpn.company.com arn:aws:acm:us-east-1:*****:certificate/*****-2056-459c-ad9c-*****
barry.bonds.aws-vpn.company.com arn:aws:acm:us-east-1:*****:certificate/*****-562d-4792-a783-*****
alice.anderson.aws-vpn.company.com arn:aws:acm:us-east-1:*****:certificate/*****-96ed-47c7-ba78-*****
AWS Client VPN Download
Mac and Windows user can download the desktop client from AWS and load the .ovpn
config file.
For iPhone users, you can download the OpenVPN Connect app from the App Store.