Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save bast7/c304a5471ac180104fe66ddc1453db3f to your computer and use it in GitHub Desktop.
Save bast7/c304a5471ac180104fe66ddc1453db3f to your computer and use it in GitHub Desktop.
Roll your own VPN with AWS CloudFormation - part three
# Credit to John Creecy
# Original can be found at https://gist.github.com/zugdud/b39eea02faa6926305f57fbde8d31b68
AWSTemplateFormatVersion: '2010-09-09'
Description: OpenVPN Stack
Parameters:
OpenVPNPort:
Type: Number
Default: 1194
Description: OpenVPN UDP port
ClientIPCIDR:
Type: String
Default: 0.0.0.0/0
Description: CIDR IP to be granted access by the SG, use 0.0.0.0/0 to accept all IPs
Mappings:
RegionMap:
us-east-1:
"AMAZONLINUXAMI" : "ami-8c1be5f6" # Amazon Linux AMI 2017.09
us-east-2:
"AMAZONLINUXAMI" : "ami-c5062ba0" # Amazon Linux AMI 2017.09
us-west-1:
"AMAZONLINUXAMI" : "ami-02eada62" # Amazon Linux AMI 2017.09
us-west-2:
"AMAZONLINUXAMI" : "ami-e689729e" # Amazon Linux AMI 2017.09
ca-central-1:
"AMAZONLINUXAMI" : "ami-fd55ec99" # Amazon Linux AMI 2017.09
eu-west-1:
"AMAZONLINUXAMI" : "ami-acd005d5" # Amazon Linux AMI 2017.09
eu-central-1:
"AMAZONLINUXAMI" : "ami-c7ee5ca8" # Amazon Linux AMI 2017.09
eu-west-2:
"AMAZONLINUXAMI" : "ami-1a7f6d7e" # Amazon Linux AMI 2017.09
ap-southeast-1:
"AMAZONLINUXAMI" : "ami-0797ea64" # Amazon Linux AMI 2017.09
ap-southeast-2:
"AMAZONLINUXAMI" : "ami-8536d6e7" # Amazon Linux AMI 2017.09
ap-northeast-2:
"AMAZONLINUXAMI" : "ami-9bec36f5" # Amazon Linux AMI 2017.09
ap-northeast-1:
"AMAZONLINUXAMI" : "ami-2a69be4c" # Amazon Linux AMI 2017.09
ap-south-1:
"AMAZONLINUXAMI" : "ami-4fc58420" # Amazon Linux AMI 2017.09
sa-east-1:
"AMAZONLINUXAMI" : "ami-f1344b9d" # Amazon Linux AMI 2017.09
Resources:
# Our VPC, most of our resources will be provisioned within
myVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/22 # We only need 1 IPaddress for our OpenVPN server, I just like even numbers and 8-bit subnets
Tags:
- Key: Name
Value: personal-OpenVPN-vpc
# The only subnet we will create within our VPC, our OpenVPN server will be provisioned within
# This subnet will be assigned a default route out to the internet, hence the name.
MyPublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref myVPC
CidrBlock: 10.0.0.0/24 # 8-bit subnet provides 256 addresses, 251 of which are usable
Tags:
- Key: Name
Value: personal-OpenVPN-publicSubnet
# We will need our VPC to have access to the internet
myInternetGateway:
Type: "AWS::EC2::InternetGateway"
Properties:
Tags:
- Key: Name
Value: personal-OpenVPN-myIGW
# The VPC route table
myRouteTablePublic:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref myVPC
Tags:
- Key: Name
Value: personal-OpenVPN-myRouteTablePublic
# Attach the Internet Gateway to myVPC
AttachInternetGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref myVPC
InternetGatewayId: !Ref myInternetGateway
# Add a default route to our VPCs internet gateway
RouteDefaultPublic:
Type: "AWS::EC2::Route"
DependsOn: myInternetGateway
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref myInternetGateway
RouteTableId: !Ref myRouteTablePublic
# Associate our route table to our subnet
MyPublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref MyPublicSubnet
RouteTableId: !Ref myRouteTablePublic
# Request a new Elastic IP Address
myEIP:
Type: "AWS::EC2::EIP"
Properties:
Domain: vpc
# Bind our Elastic IP Address to an Elastic Network Interface
AssociateManagementAccessPort:
Type: AWS::EC2::EIPAssociation
Properties:
AllocationId: !GetAtt myEIP.AllocationId
NetworkInterfaceId: !Ref myNetworkInterface
# Create a security group for the ENI that will be attached to our OpenVPN server
# OpenVPN and SSH port access
OpenVPNInstanceSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: SG for OpenVPN Server
VpcId: !Ref myVPC
SecurityGroupIngress:
- IpProtocol: udp
FromPort: !Ref OpenVPNPort
ToPort: !Ref OpenVPNPort
CidrIp: !Ref ClientIPCIDR
# This is the IAM role which will be associated with our EC2 instance
myEC2InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
# This is the IAM policy which will be attached to our EC2 instance role
myAccessPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: myAccessPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- s3:*
Effect: Allow
Resource: "*"
Roles:
- !Ref myEC2InstanceRole
# Binding profile for our myEC2InstanceRole to the actual EC2 instance
ec2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: "/"
Roles:
- !Ref myEC2InstanceRole
# The Elastic Network Interface which will be attached to our EC2 instance
# Our security group, OpenVPNInstanceSG is also associated with this interface
myNetworkInterface:
Type: AWS::EC2::NetworkInterface
Properties:
SubnetId: !Ref MyPublicSubnet
Description: Public Interface
GroupSet:
- !Ref OpenVPNInstanceSG
SourceDestCheck: false
Tags:
-
Key: Name
Value: Public ENI
# This is the S3 bucket where our client profile and secrets will be stored
myS3Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: Private
# The EC2 instance which will host OpenVPN
EC2OpenVPNInstance:
Type: "AWS::EC2::Instance"
Properties:
ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMAZONLINUXAMI]
InstanceType: t2.micro
SourceDestCheck: false
NetworkInterfaces:
-
NetworkInterfaceId: !Ref myNetworkInterface
DeviceIndex: 0
IamInstanceProfile: !Ref ec2InstanceProfile
Tags:
-
Key: Name
Value: OpenVPN Server
# User data is passed into the instance, executed as a shell script, and run only once on first boot
# Here we invoke cfn-init on our configSet myCfnConfigSet
# The last command emits a cfn-signal to the CloudFormation stack which completes the associated CreationPolicy timer
UserData:
"Fn::Base64":
!Sub |
#!/bin/bash
yum update -y aws-cfn-bootstrap
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2OpenVPNInstance --configsets myCfnConfigSet --region ${AWS::Region}
yum -y update
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource EC2OpenVPNInstance --region ${AWS::Region}
# The CloudFormation stack will wait to mark the EC2OpenVPNInstance as CREATE_COMPLETE until we recieve a signal from the instance, or 10 minutes elapses.
CreationPolicy:
ResourceSignal:
Count: "1"
Timeout: PT10M
Metadata:
AWS::CloudFormation::Init:
# Our cfn-init config set rules, divided into logical sections to make reading it easier, hopefully :)
configSets:
myCfnConfigSet:
- "configure_cfn"
- "install_software"
- "generate_secrets"
- "generate_client"
- "configure_server"
- "upload_files"
# Configure and start cfn-hup
# cfn-hup will poll the stack for changes, and if possible, apply instance changes in place on the instance
configure_cfn:
files:
/etc/cfn/hooks.d/cfn-auto-reloader.conf:
content: !Sub |
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.EC2OpenVPNInstance.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2OpenVPNInstance --configsets myCfnConfigSet --region ${AWS::Region}
mode: "000400"
owner: root
group: root
/etc/cfn/cfn-hup.conf:
content: !Sub |
[main]
stack=${AWS::StackId}
region=${AWS::Region}
verbose=true
interval=1
mode: "000400"
owner: root
group: root
services:
sysvinit:
cfn-hup:
enabled: "true"
ensureRunning: "true"
files:
- "/etc/cfn/cfn-hup.conf"
- "/etc/cfn/hooks.d/cfn-auto-reloader.conf"
# Install the latest version of openvpn via the yum package manager
# Install easy-rsa via the EPEL repo
# Make a copy of the installed files to /opt/easy-rsa as our working directory
install_software:
packages:
yum:
openvpn: []
commands:
01_install_software_install_easyrsa:
command: "yum install easy-rsa -y --enablerepo=epel"
02_install_software_copy_easyrsa:
command: "cp -R /usr/share/easy-rsa/2.0 /opt/easy-rsa"
# Use easy-rsa to generate our certificate authority (CA) and encryption keys
# I'm not sure if it's possible to source files into the cfn-init environment, so I am just doing it inline with each command
# The easy-rsa scripts use an interactive mode flag which is what the sed command is removing
# Use openssl to generate a static TLS client cert, this is what the client will use authenticate with the the OpenVPN server
generate_secrets:
commands:
01_generate_secrets_clean_keysdir:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/clean-all"
command: "source /opt/easy-rsa/vars;/opt/easy-rsa/clean-all"
02_generate_secrets_update_build-ca:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/build-ca"
command: !Sub |
sed -i 's/--interact//g' /opt/easy-rsa/build-ca
03_generate_secrets_run_build-ca:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/build-ca"
command: "source /opt/easy-rsa/vars;/opt/easy-rsa/build-ca"
04_generate_secrets_run_build-dh:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/build-dh"
command: "source /opt/easy-rsa/vars;/opt/easy-rsa/build-dh"
05_generate_secrets_update_build-key-server:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/build-key-server"
command: !Sub |
sed -i 's/--interact//g' /opt/easy-rsa/build-key-server
06_generate_secrets_run_build-key-server:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/build-key-server"
command: "source /opt/easy-rsa/vars;/opt/easy-rsa/build-key-server server"
07_generate_secrets_statictlssecret:
cwd: "/opt/easy-rsa/keys"
command: "openvpn --genkey --secret statictlssecret.key"
# Generate the openvpn client configuration files
# Generate a script which will concatinate the client configuration with the cert and encryption key to generate the ovpn profile
generate_client:
files:
/opt/easy-rsa/openvpn_client.conf:
content: !Sub |
client
dev tun
proto udp
remote ${myEIP} ${OpenVPNPort}
ca ca.crt
cert clientuser.crt
key clientuser.key
tls-client
tls-auth statictlssecret.key 1
tls-version-min 1.2
tls-cipher TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256
cipher AES-256-CBC
auth SHA512
resolv-retry infinite
auth-retry none
nobind
persist-key
persist-tun
ns-cert-type server
comp-lzo
verb 3
mode: "000700"
owner: root
group: root
/opt/easy-rsa/gen_ovpn_profile.sh:
content: !Sub |
(cat /opt/easy-rsa/openvpn_client.conf
echo '<key>'
cat keys/clientuser.key
echo '</key>'
echo '<cert>'
cat keys/clientuser.crt
echo '</cert>'
echo '<ca>'
cat keys/ca.crt
echo '</ca>'
) > /opt/easy-rsa/keys/openvpn_clientuser.ovpn
mode: "000700"
owner: root
group: root
commands:
01_generate_client_update_build-key:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/build-key"
command: !Sub |
sed -i 's/--interact//g' /opt/easy-rsa/build-key
02_generate_client_run_build-key:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/build-key"
command: "source /opt/easy-rsa/vars;/opt/easy-rsa/build-key clientuser"
03_generate_client_generate_ovpn_profile:
cwd: "/opt/easy-rsa"
test: "test -e /opt/easy-rsa/gen_ovpn_profile.sh"
command: "/opt/easy-rsa/gen_ovpn_profile.sh"
# Generate configuration file for the OpenVPN server
# Enable IP forwarding in Linux
# Start OpenVPN
configure_server:
files:
/opt/openvpn/server.conf:
content: !Sub |
port ${OpenVPNPort}
proto udp
dev tun
server 172.16.0.0 255.255.252.0
push "redirect-gateway def1"
ca /opt/easy-rsa/keys/ca.crt
cert /opt/easy-rsa/keys/server.crt
key /opt/easy-rsa/keys/server.key
dh /opt/easy-rsa/keys/dh2048.pem
tls-server
tls-auth /opt/easy-rsa/keys/statictlssecret.key 0
tls-version-min 1.2
tls-cipher TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256
cipher AES-256-CBC
auth SHA512
ifconfig-pool-persist ipp.txt
keepalive 10 120
ping-timer-rem
comp-lzo
persist-key
persist-tun
status openvpn-status.log
log-append /var/log/openvpn.log
verb 3
max-clients 100
user nobody
group nobody
mode: "000644"
owner: "root"
group: "root"
commands:
01_configure_server_sysctl_ipforward:
command: echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
02_configure_server_sysctl_reload:
command: "sysctl -p"
03_configure_server_iptables_nat:
command: "iptables -t nat -A POSTROUTING -s 172.16.0.0/22 -o eth0 -j MASQUERADE"
04_configure_server_update_config:
command: "cp -rf /opt/openvpn/server.conf /etc/openvpn/server.conf"
05_configure_server_openvpn_start:
command: "service openvpn start"
# Zip the client files
# Upload the client file archive and cfn-init log to S3
upload_files:
commands:
01_upload_files_zipfiles:
cwd: "/opt/easy-rsa/keys"
command: "zip openVPNClientFiles.zip ca.crt statictlssecret.key clientuser.key clientuser.crt openvpn_clientuser.ovpn"
02_upload_files_s3cp_openVPNClientFiles:
cwd: "/opt/easy-rsa/keys"
command: !Sub |
aws s3 cp openVPNClientFiles.zip s3://${myS3Bucket}/client/openVPNClientFiles.zip
03_upload_files_s3cp_cfn_init_log:
cwd: "/var/log"
test: "test -e /var/log/cfn-init.log"
command: !Sub |
aws s3 cp /var/log/cfn-init.log s3://${myS3Bucket}/log/genSecrets_cfn-init.log
# The role which our Lambda function will assume when it executes
EmptyS3BucketExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
# The policy which will be attached to our Lambda execution role
# The Lambda function will need access to write output to CloudWatch Logs and full access to S3
EmptyS3BucketExecutionPolicy:
DependsOn:
- EmptyS3BucketExecutionRole
Type: AWS::IAM::Policy
Properties:
PolicyName: EmptyS3BucketLogsRolePolicy
Roles:
- Ref: EmptyS3BucketExecutionRole
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:*
Resource:
- arn:aws:logs:*:*:*
- Effect: Allow
Action:
- s3:*
Resource:
- "*"
# This lambda function will empty and delete the S3 bucket which was created with our stack
# The function will be invoked on stack creation, update, and deletion events fired from CloudFormation
EmptyS3BucketLambda:
Type: AWS::Lambda::Function
DependsOn:
- EmptyS3BucketExecutionRole
- EmptyS3BucketExecutionPolicy
Properties:
Timeout: 60
Handler: index.handler
Runtime: "nodejs6.10"
MemorySize: 128
Code:
ZipFile: !Sub |
'use strict';
exports.handler = (event, context) => {
console.log(JSON.stringify(event));
var responseObject = {};
responseObject.event = event;
responseObject.context = context;
if (event.RequestType == "Delete") {
console.log("Handle Delete Event");
var AWS = require('aws-sdk');
var s3Client = new AWS.S3();
clearBucket(s3Client, event.ResourceProperties.S3BucketName, responseObject);
} else {
console.log("Handle Create Event");
sendResponse(responseObject);
}
};
function sendResponse(responseObject) {
var cfnResponse = require('cfn-response');
cfnResponse.send(responseObject.event, responseObject.context, cfnResponse.SUCCESS);
}
function deleteBucket(s3Client, bucket, responseObject) {
s3Client.deleteBucket({
Bucket: bucket
}, function(err, data) {
if (err) {
console.log(err, err.stack);
} else {
console.log(data);
}
sendResponse(responseObject);
});
}
function clearBucket(s3Client, bucket, responseObject) {
s3Client.listObjects({
Bucket: bucket
}, function(err, data) {
if (err) {
console.log("error listing bucket objects " + err);
return;
}
var items = data.Contents;
for (var i = 0; i < items.length; i += 1) {
var deleteParams = {
Bucket: bucket,
Key: items[i].Key
};
deleteObject(s3Client, deleteParams);
}
});
setTimeout(function() {
deleteBucket(s3Client, bucket, responseObject);
}, 10000);
}
function deleteObject(s3Client, deleteParams) {
s3Client.deleteObject(deleteParams, function(err, data) {
if (err) {
console.log("Error Deleting Object: " + deleteParams.Key);
} else {
console.log("Deleted Object: " + deleteParams.Key);
}
});
}
Role: !GetAtt EmptyS3BucketExecutionRole.Arn
# Custom resource for the EmptyS3BucketLambda lambda function
# CloudFormation will wait for the associated lambda function to signal back with cfn-response
EmptyS3BucketAction:
Type: Custom::EmptyS3BucketAction
Properties:
ServiceToken: !GetAtt EmptyS3BucketLambda.Arn
Region: !Ref "AWS::Region"
S3BucketName: !Ref myS3Bucket
Outputs:
myS3BucketOut:
Description: S3 bucket name
Value: !Ref myS3Bucket
myEIPOut:
Description: Instance EIP
Value: !Ref myEIP
EC2OpenVPNInstanceOut:
Description: EC2 Instance
Value: !Ref EC2OpenVPNInstance
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment