Forked from pbzona/vpn-cloudformation-template.yaml
Last active
May 22, 2024 02:50
-
-
Save ScriptAutomate/0f297032f5d538da4dd059834c4bb14d to your computer and use it in GitHub Desktop.
Roll your own Amazon Linux 2 OpenVPN with AWS CloudFormation (w/ Dynamically Discovered Latest AMI Id via Parameter Store)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Personal OpenVPN Server Deployment | |
# Updates applied by Derek Ardolf / @ScriptAutomate / https://icanteven.io | |
# 11/08/2019 - Version 2.x: | |
# - Released with many updates, and tracked here: https://github.com/ScriptAutomate/openvpn-cfn | |
# | |
# Created by John Creecy / @zugdug | |
# 10/30/2017 - Version 1.0: | |
# - Original: https://gist.github.com/zugdud/b39eea02faa6926305f57fbde8d31b68 | |
# Original Linux Academy blog series walkthrough on building the old OpenVPN template: | |
# - Part 1: https://linuxacademy.com/blog/tutorials/roll-vpn-aws-cloudformation-part-one/ | |
# - Part 2: https://linuxacademy.com/blog/tutorials/roll-vpn-aws-cloudformation-part-two/ | |
# - Part 3: https://linuxacademy.com/blog/tutorials/how-to-roll-your-own-vpn-with-aws-cloudformation-part-three/ | |
AWSTemplateFormatVersion: '2010-09-09' | |
Description: OpenVPN Stack | |
Parameters: | |
OpenVPNProtocol: | |
Type: String | |
Default: udp | |
AllowedValues: | |
- udp | |
- tcp | |
OpenVPNPort: | |
Type: Number | |
Default: 1194 | |
Description: OpenVPN port. Standards - 443 for TCP / 1194 for UDP | |
AllowedValues: | |
- 443 | |
- 1194 | |
SSHKeyName: | |
Type: AWS::EC2::KeyPair::KeyName | |
Description: SSH Key for the OpenVPN Instance | |
ConstraintDescription: Must be the name of an existing EC2 KeyPair. | |
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 | |
AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$' | |
# Parameter store makes finding latest AMI image id easy | |
# Is region locked, by default. Wherever CFN is spun up, | |
# resulting image id is specific to source region! | |
# Source: https://aws.amazon.com/blogs/compute/query-for-the-latest-amazon-linux-ami-ids-using-aws-systems-manager-parameter-store/ | |
LatestAmiId: | |
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>' | |
Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' | |
OpenVPNVersion: | |
Type: "String" | |
Default: "2.4.7" # Latest Tested Version on Amazon Linux 2 | |
EasyRSAREQCN: | |
Type: "String" | |
Default: "ChangeMe" | |
EasyRSAREQCNAlgorithm: | |
Type: 'String' | |
Default: rsa | |
AllowedValues: | |
- rsa | |
- ec | |
Resources: | |
# Our VPC, most of our resources will be provisioned within | |
OpenVPNVPC: | |
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 OpenVPNVPC | |
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 OpenVPNVPC | |
Tags: | |
- Key: Name | |
Value: personal-OpenVPN-myRouteTablePublic | |
# Attach the Internet Gateway to OpenVPNVPC | |
AttachInternetGateway: | |
Type: AWS::EC2::VPCGatewayAttachment | |
Properties: | |
VpcId: !Ref OpenVPNVPC | |
InternetGatewayId: !Ref myInternetGateway | |
# Add a default route to our VPCs internet gateway | |
RouteDefaultPublic: | |
Type: "AWS::EC2::Route" | |
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 | |
OpenVPNEIP: | |
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 OpenVPNEIP.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 OpenVPNVPC | |
SecurityGroupIngress: | |
- IpProtocol: !Ref OpenVPNProtocol | |
FromPort: !Ref OpenVPNPort | |
ToPort: !Ref OpenVPNPort | |
CidrIp: !Ref ClientIPCIDR | |
- IpProtocol: tcp | |
FromPort: 22 | |
ToPort: 22 | |
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 S3 bucket where our client profile and secrets will be stored | |
OpenVPNS3Bucket: | |
Type: AWS::S3::Bucket | |
Properties: | |
AccessControl: Private | |
# This is the IAM policy which will be attached to our EC2 instance role | |
OpenVPNAccessPolicy: | |
Type: AWS::IAM::Policy | |
Properties: | |
PolicyName: OpenVPNAccessPolicy | |
PolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Action: | |
- s3:* | |
Effect: Allow | |
Resource: !Join | |
- '' | |
- - 'arn:aws:s3:::' | |
- !Ref OpenVPNS3Bucket | |
- /* | |
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 | |
# The EC2 instance which will host OpenVPN | |
EC2OpenVPNInstance: | |
Type: "AWS::EC2::Instance" | |
DependsOn: OpenVPNS3Bucket | |
Properties: | |
ImageId: !Ref LatestAmiId | |
InstanceType: t2.micro | |
SourceDestCheck: false | |
KeyName: !Ref SSHKeyName | |
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 install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm | |
yum update -y | |
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource EC2OpenVPNInstance --configsets myCfnConfigSet --region ${AWS::Region} | |
/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 receive 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_easyrsa_vars" | |
- "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: [ !Ref OpenVPNVersion ] | |
easy-rsa: [] | |
yum-plugin-versionlock: [] | |
commands: | |
01_install_software_copy_easyrsa: | |
command: "cp -RL /usr/share/easy-rsa/3 /opt/easy-rsa" | |
02_lock_openvpn_version: | |
command: "yum versionlock openvpn" | |
# Default is rsa for algorithm | |
# Changing to ECDSA | |
generate_easyrsa_vars: | |
files: | |
/opt/easy-rsa/vars: | |
content: !Sub | | |
set_var EASYRSA_ALGO ${EasyRSAREQCNAlgorithm} | |
set_var EASYRSA_REQ_CN ${EasyRSAREQCN} | |
mode: "000644" | |
owner: "root" | |
group: "root" | |
# Use easy-rsa to generate our certificate authority (CA) and encryption keys | |
# Use openvpn 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_init_pkidir: | |
cwd: "/opt/easy-rsa" | |
test: "test -e /opt/easy-rsa/easyrsa" | |
command: "/opt/easy-rsa/easyrsa init-pki" | |
02_generate_secrets_run_build-ca: | |
cwd: "/opt/easy-rsa" | |
command: "/opt/easy-rsa/easyrsa --batch build-ca nopass" | |
03_generate_secrets_run_build-dh: | |
cwd: "/opt/easy-rsa" | |
command: "/opt/easy-rsa/easyrsa gen-dh" | |
04_generate_secrets_run_build-server-full: | |
cwd: "/opt/easy-rsa" | |
command: "/opt/easy-rsa/easyrsa build-server-full server nopass" | |
05_generate_empty_revoke_list: | |
cwd: "/opt/easy-rsa" | |
command: "/opt/easy-rsa/easyrsa gen-crl" | |
06_generate_secrets_statictlssecret: | |
cwd: "/opt/easy-rsa" | |
command: "openvpn --genkey --secret pki/statictlssecret.key" | |
# Generate the openvpn client configuration files | |
# Generate a script which will collect the client configuration into an OVPN profile | |
generate_client: | |
files: | |
/opt/easy-rsa/openvpn_client.conf: | |
content: !Sub | | |
client | |
dev tun | |
proto ${OpenVPNProtocol} | |
remote ${OpenVPNEIP} ${OpenVPNPort} | |
tls-client | |
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 | |
remote-cert-tls server | |
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 pki/private/clientuser.key | |
echo '</key>' | |
echo '<cert>' | |
cat pki/issued/clientuser.crt | |
echo '</cert>' | |
echo '<ca>' | |
cat pki/ca.crt | |
echo '</ca>' | |
echo 'key-direction 1' | |
echo '<tls-auth>' | |
cat pki/statictlssecret.key | |
echo '</tls-auth>' | |
) > /opt/easy-rsa/pki/openvpn_clientuser.ovpn | |
mode: "000700" | |
owner: root | |
group: root | |
commands: | |
01_generate_client_run_build-key: | |
cwd: "/opt/easy-rsa" | |
command: "/opt/easy-rsa/easyrsa build-client-full clientuser nopass" | |
02_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 ${OpenVPNProtocol} | |
dev tun | |
server 172.16.0.0 255.255.252.0 | |
push "redirect-gateway def1" | |
ca /opt/easy-rsa/pki/ca.crt | |
cert /opt/easy-rsa/pki/issued/server.crt | |
key /opt/easy-rsa/pki/private/server.key | |
dh /opt/easy-rsa/pki/dh.pem | |
tls-server | |
tls-auth /opt/easy-rsa/pki/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 | |
persist-key | |
persist-tun | |
status openvpn-status.log | |
log-append /var/log/openvpn.log | |
verb 3 | |
max-clients 5 | |
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: | |
test: "test -e /opt/openvpn/server.conf" | |
command: "cp -f /opt/openvpn/server.conf /etc/openvpn/server/server.conf" | |
05_configure_systemctl_openvpn_enable: | |
test: "test -e /lib/systemd/system/[email protected]" | |
command: "systemctl enable openvpn-server@server" | |
06_configure_systemctl_openvpn_start: | |
test: "test -e /lib/systemd/system/[email protected]" | |
command: "systemctl start openvpn-server@server" | |
# Zip the client files | |
# Upload the client file archive and cfn-init log to S3 | |
upload_files: | |
commands: | |
01_upload_file_openvpn-clientuser.ovpn: | |
cwd: "/opt/easy-rsa/pki" | |
command: !Sub | | |
aws s3 cp openvpn_clientuser.ovpn s3://${OpenVPNS3Bucket}/client/openvpn_clientuser.ovpn | |
02_upload_file_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://${OpenVPNS3Bucket}/log/genSecrets_cfn-init.log | |
# Original Source for the following Python 3.7 CustomResource section: | |
# https://github.com/mike-mosher/custom-resource-s3-bucket-delete | |
EmptyS3BucketLambdaExecutionRole: | |
Type: "AWS::IAM::Role" | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: 2012-10-17 | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: lambda.amazonaws.com | |
Action: | |
- "sts:AssumeRole" | |
Policies: | |
- PolicyName: LoggingPolicy | |
PolicyDocument: | |
Version: 2012-10-17 | |
Statement: | |
- Effect: Allow | |
Action: | |
- logs:CreateLogGroup | |
- logs:CreateLogStream | |
- logs:PutLogEvents | |
Resource: "*" | |
- PolicyName: S3Policy | |
PolicyDocument: | |
Version: 2012-10-17 | |
Statement: | |
- Effect: Allow | |
Action: | |
- s3:List* | |
- s3:DeleteObject | |
Resource: "*" | |
EmptyS3BucketLambdaFunction: | |
Type: "AWS::Lambda::Function" | |
Properties: | |
Code: | |
ZipFile: | | |
import cfnresponse | |
import boto3 | |
def handler(event, context): | |
print(event) | |
print('boto version ' + boto3.__version__) | |
# Globals | |
responseData = {} | |
ResponseStatus = cfnresponse.SUCCESS | |
s3bucketName = event['ResourceProperties']['s3bucketName'] | |
if event['RequestType'] == 'Create': | |
responseData['Message'] = "Resource creation successful!" | |
elif event['RequestType'] == 'Update': | |
responseData['Message'] = "Resource update successful!" | |
elif event['RequestType'] == 'Delete': | |
# Need to empty the S3 bucket before it is deleted | |
s3 = boto3.resource('s3') | |
bucket = s3.Bucket(s3bucketName) | |
bucket.objects.all().delete() | |
responseData['Message'] = "Resource deletion successful!" | |
cfnresponse.send(event, context, ResponseStatus, responseData) | |
Handler: index.handler | |
Runtime: python3.7 | |
Role: !GetAtt EmptyS3BucketLambdaExecutionRole.Arn | |
EmptyS3Bucket: | |
Type: Custom::CustomResource | |
Properties: | |
ServiceToken: !GetAtt EmptyS3BucketLambdaFunction.Arn | |
s3bucketName: !Ref OpenVPNS3Bucket | |
Outputs: | |
VPNClientsS3Bucket: | |
Description: S3 bucket name | |
Value: !Ref OpenVPNS3Bucket | |
VPNClientUserFile: | |
Description: Initial generated OVPN of clientuser | |
Value: !Sub | | |
s3://${OpenVPNS3Bucket}/client/openvpn_clientuser.ovpn | |
OpenVPNEIP: | |
Description: Instance EIP | |
Value: !Ref OpenVPNEIP | |
EC2OpenVPNInstance: | |
Description: EC2 Instance | |
Value: !Ref EC2OpenVPNInstance |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Any future modifications of this template are now located here: