OpenVpn via Cloudformation

Sat 27 February 2016 by Patrick Pierson

The world needs more encryption. After the recent announcement by Apple, I was motivated to use my knowledge of Cloudformation and OpenVPN that I gained through my employment at Berico Technologies and Selection Pressure/IonChannel.

Cloudformation (CF) is a service that allows you to start up Amazon Web Services (AWS) resources using a very common format, JavaScript Object Notation (JSON), to write infrastructure as code. CF took me a while to figure out and there are some addons like Troposphere, written in python, that allow you to write CF in other languages.

In this article, I will go into detail about the various sections in my JSON format CF template. I do not use Troposphere for this but in the future I will most likely do so because the process ends up being a lot easier. An add on to Troposphere is Environmentbase which makes deploying CF even easier. I believe to get to Troposphere and Environmentbase you need to have a deep understanding of CF.

To start off we need to setup some input parameters and mappings. In the "Parameters" section the following allows for inputs to be specified.

This block specifies the EC2 KeyPair to be used for the Webserver group. The type used is an AWS-specific parameter type that specifically knows about the EC2 KeyPairs available in the region used.

"KeyName": {
  "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
  "Type": "AWS::EC2::KeyPair::KeyName",
  "ConstraintDescription" : "must be the name of an existing EC2 KeyPair."
}

The next block specifies the InstanceType for the Webserver group. I used a default of t2.small that gives us 2GB of ram and 1 vCPU but more importantly this is a VPN that gives us 20-50Mbps of network traffic. In the event the instance is slow to respond or network speed is low because you have 100s of users, you will want to change this setting to something higher. The AllowedValues are all known instance types but could also be changed to only allow smaller instance types if needed.

"InstanceType" : {
  "Description" : "WebServer EC2 instance type",
  "Type" : "String",
  "Default" : "t2.small",
  "AllowedValues" : [ "t1.micro", "t2.nano", "t2.micro", "t2.small", "t2.medium", "t2.large", "m1.small", "m1.medium", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "m3.medium", "m3.large", "m3.xlarge", "m3.2xlarge", "m4.large", "m4.xlarge", "m4.2xlarge", "m4.4xlarge", "m4.10xlarge", "c1.medium", "c1.xlarge", "c3.large", "c3.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge", "c4.large", "c4.xlarge", "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", "g2.2xlarge", "g2.8xlarge", "r3.large", "r3.xlarge", "r3.2xlarge", "r3.4xlarge", "r3.8xlarge", "i2.xlarge", "i2.2xlarge", "i2.4xlarge", "i2.8xlarge", "d2.xlarge", "d2.2xlarge", "d2.4xlarge", "d2.8xlarge", "hi1.4xlarge", "hs1.8xlarge", "cr1.8xlarge", "cc2.8xlarge", "cg1.4xlarge"],
  "ConstraintDescription" : "must be a valid EC2 instance type."
}

To be honest the SSH location is actually a bad name for this parameter. I copied a large majority of this template from the Wordpress CF template that AWS provides. Later on you will see I actually reference this in the security group not only for SSH access but also for the VPN port. The default is set to 0.0.0.0/0 to allow all entry.

"SSHLocation": {
  "Description": "The IP address range that can be used to SSH to the EC2 instances",
  "Type": "String",
  "MinLength": "9",
  "MaxLength": "18",
  "Default": "0.0.0.0/0",
  "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
  "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x."
}

WebServerCapacity is the minimum and maximum number of instances the auto scaling group (ASG). Later on you will see the desired number is set to 1. In this setup OpenVPN will only work with one instance but is in place to allow for the automatic scale up and down of the ASG.

"WebServerCapacity": {
  "Default": "1",
  "Description" : "The initial number of WebServer instances",
  "Type": "Number",
  "MinValue": "1",
  "MaxValue": "5",
  "ConstraintDescription" : "must be between 1 and 5 EC2 instances."
}

Next comes mappings. Mappings allow you to set static key/value named entries. Usually you will see the instance types mapped to the specific architecture type. I have also taken it a step further and mapped my Virtual Private Cloud (VPC) and subnet CIDR ranges.

  "Mappings" : {
    "SubnetConfig" : {
      "VPC"     : { "CIDR" : "10.44.0.0/16" },
      "Public0" : { "CIDR" : "10.44.0.0/24" },
      "Public1" : { "CIDR" : "10.44.1.0/24" }
    },

The next set of mappings is what you normally see in the mapping section. This maps the instance type to architecture type.

"AWSInstanceType2Arch" : {
    "t1.micro"    : { "Arch" : "PV64"   }, "t2.nano"     : { "Arch" : "HVM64"  },
    "t2.micro"    : { "Arch" : "HVM64"  }, "t2.small"    : { "Arch" : "HVM64"  }, 
    "t2.medium"   : { "Arch" : "HVM64"  }, "t2.large"    : { "Arch" : "HVM64"  },
    "m1.small"    : { "Arch" : "PV64"   }, "m1.medium"   : { "Arch" : "PV64"   }, 
    "m1.large"    : { "Arch" : "PV64"   }, "m1.xlarge"   : { "Arch" : "PV64"   }, 
    "m2.xlarge"   : { "Arch" : "PV64"   }, "m2.2xlarge"  : { "Arch" : "PV64"   },
    "m2.4xlarge"  : { "Arch" : "PV64"   }, "m3.medium"   : { "Arch" : "HVM64"  }, 
    "m3.large"    : { "Arch" : "HVM64"  }, "m3.xlarge"   : { "Arch" : "HVM64"  }, 
    "m3.2xlarge"  : { "Arch" : "HVM64"  }, "m4.large"    : { "Arch" : "HVM64"  },
    "m4.xlarge"   : { "Arch" : "HVM64"  }, "m4.2xlarge"  : { "Arch" : "HVM64"  }, 
    "m4.4xlarge"  : { "Arch" : "HVM64"  }, "m4.10xlarge" : { "Arch" : "HVM64"  }, 
    "c1.medium"   : { "Arch" : "PV64"   }, "c1.xlarge"   : { "Arch" : "PV64"   },
    "c3.large"    : { "Arch" : "HVM64"  }, "c3.xlarge"   : { "Arch" : "HVM64"  }, 
    "c3.2xlarge"  : { "Arch" : "HVM64"  }, "c3.4xlarge"  : { "Arch" : "HVM64"  }, 
    "c3.8xlarge"  : { "Arch" : "HVM64"  }, "c4.large"    : { "Arch" : "HVM64"  },
    "c4.xlarge"   : { "Arch" : "HVM64"  }, "c4.2xlarge"  : { "Arch" : "HVM64"  }, 
    "c4.4xlarge"  : { "Arch" : "HVM64"  }, "c4.8xlarge"  : { "Arch" : "HVM64"  }, 
    "g2.2xlarge"  : { "Arch" : "HVMG2"  }, "g2.8xlarge"  : { "Arch" : "HVMG2"  },
    "r3.large"    : { "Arch" : "HVM64"  }, "r3.xlarge"   : { "Arch" : "HVM64"  }, 
    "r3.2xlarge"  : { "Arch" : "HVM64"  }, "r3.4xlarge"  : { "Arch" : "HVM64"  }, 
    "r3.8xlarge"  : { "Arch" : "HVM64"  }, "i2.xlarge"   : { "Arch" : "HVM64"  },
    "i2.2xlarge"  : { "Arch" : "HVM64"  }, "i2.4xlarge"  : { "Arch" : "HVM64"  }, 
    "i2.8xlarge"  : { "Arch" : "HVM64"  }, "d2.xlarge"   : { "Arch" : "HVM64"  }, 
    "d2.2xlarge"  : { "Arch" : "HVM64"  }, "d2.4xlarge"  : { "Arch" : "HVM64"  },
    "d2.8xlarge"  : { "Arch" : "HVM64"  }, "hi1.4xlarge" : { "Arch" : "HVM64"  }, 
    "hs1.8xlarge" : { "Arch" : "HVM64"  }, "cr1.8xlarge" : { "Arch" : "HVM64"  }, 
    "cc2.8xlarge" : { "Arch" : "HVM64"  }
},

Another section of mappings you will normally see is the section that sets which Amazon Machine Image (AMI) Id to which region for the specific architecture type.

"AWSRegionArch2AMI" : {
  "us-east-1"        : {"PV64" : "ami-5fb8c835", "HVM64" : "ami-60b6c60a", "HVMG2" : "ami-e998ea83"},
  "us-west-2"        : {"PV64" : "ami-d93622b8", "HVM64" : "ami-f0091d91", "HVMG2" : "ami-315f4850"},
  "us-west-1"        : {"PV64" : "ami-56ea8636", "HVM64" : "ami-d5ea86b5", "HVMG2" : "ami-943956f4"},
  "eu-west-1"        : {"PV64" : "ami-95e33ce6", "HVM64" : "ami-bff32ccc", "HVMG2" : "ami-83fd23f0"},
  "eu-central-1"     : {"PV64" : "ami-794a5915", "HVM64" : "ami-bc5b48d0", "HVMG2" : "ami-ba1a09d6"},
  "ap-northeast-1"   : {"PV64" : "ami-393c1957", "HVM64" : "ami-383c1956", "HVMG2" : "ami-08e5c166"},
  "ap-northeast-2"   : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-249b554a", "HVMG2" : "NOT_SUPPORTED"},
  "ap-southeast-1"   : {"PV64" : "ami-34bd7a57", "HVM64" : "ami-c9b572aa", "HVMG2" : "ami-5a15d239"},
  "ap-southeast-2"   : {"PV64" : "ami-ced887ad", "HVM64" : "ami-48d38c2b", "HVMG2" : "ami-0c1a446f"},
  "sa-east-1"        : {"PV64" : "ami-7d15ad11", "HVM64" : "ami-6817af04", "HVMG2" : "NOT_SUPPORTED"},
  "cn-north-1"       : {"PV64" : "ami-18ac6575", "HVM64" : "ami-43a36a2e", "HVMG2" : "NOT_SUPPORTED"}
}
},

This next section I copied from the Wordpress template I mentioned above I believe it has something to do with specifying which regions have VPC capability and if that region does not have it the template will default back to Elastic Compute Cloud (EC2) Classic.

"Conditions" : {
    "Is-EC2-VPC"     : { "Fn::Or" : [ {"Fn::Equals" : [{"Ref" : "AWS::Region"}, "eu-central-1" ]},
                                      {"Fn::Equals" : [{"Ref" : "AWS::Region"}, "cn-north-1" ]},
                                      {"Fn::Equals" : [{"Ref" : "AWS::Region"}, "ap-northeast-2" ]}]},
    "Is-EC2-Classic" : { "Fn::Not" : [{ "Condition" : "Is-EC2-VPC"}]}},

Finally we are to the meat of CF. Resources are the AWS services and instances we are provisioning via CF. I wanted to create a new VPC for the VPN (it is very easy to do in CF). Setting up a new VPC gives us so many more options in the future. Currently this is only a VPC to the internet but with some code changes I could easily tie it into my exisiting AWS envionments using VPC peering connections. The only things we need to set here are the CidrBlock and Tags. The CIDR block is set by looking at the mappings from before and finding the VPC CIDR I set. These tags set the application tag as the CF stack name. Network tag is set as public and the name of the VPC is called VPN VPC.

"VPC" : {
  "Type" : "AWS::EC2::VPC",
  "Properties" : {
    "CidrBlock" : { "Fn::FindInMap" : [ "SubnetConfig", "VPC", "CIDR" ]},
    "Tags" : [
      { "Key" : "Application", "Value" : { "Ref" : "AWS::StackName" } },
      { "Key" : "Network", "Value" : "Public" },
      { "Key" : "Name", "Value" : "VPN VPC" }
    ]
  }
},

Next we set the subnets. I created two subnets to allow for high availability in the event one of the Availability Zones (AZ) in AWS goes down. Both subnets depend on the VPC being created. The only properties we set here are the VpcId which we set as a reference to the VPC we created before. I hard set the AvailabilityZone for these subnets because I prefered to do that here then instead of the ASG later. The CidrBlock is set by finding it in the mappings before but looking up "Public" and the subnet number (either 0 or 1). Tags are set like before in the VPC.

"PublicSubnet0" : {
  "DependsOn" : ["VPC"],
  "Type" : "AWS::EC2::Subnet",
  "Properties" : {
    "VpcId" : { "Ref" : "VPC" },
    "AvailabilityZone" : "us-east-1a",
    "CidrBlock" : { "Fn::FindInMap" : [ "SubnetConfig", "Public0", "CIDR" ]},
    "Tags" : [
      { "Key" : "Application", "Value" : { "Ref" : "AWS::StackName" } },
      { "Key" : "Network", "Value" : "Public" },
      { "Key" : "Name", "Value" : "Public Subnet" }
    ]
  }
},
"PublicSubnet1" : {
  "DependsOn" : ["VPC"],
  "Type" : "AWS::EC2::Subnet",
  "Properties" : {
    "VpcId" : { "Ref" : "VPC" },
    "AvailabilityZone" : "us-east-1c",
    "CidrBlock" : { "Fn::FindInMap" : [ "SubnetConfig", "Public1", "CIDR" ]},
    "Tags" : [
      { "Key" : "Application", "Value" : { "Ref" : "AWS::StackName" } },
      { "Key" : "Network", "Value" : "Public" },
      { "Key" : "Name", "Value" : "Public Subnet" }
    ]
  }
},

The internet gateway is an EC2 resource that allows for traffic to pass to the internet from the VPC which is very important for a VPN. This block is a basic call to create it.

"InternetGateway" : {
  "Type" : "AWS::EC2::InternetGateway",
  "Properties" : {
    "Tags" : [
      { "Key" : "Application", "Value" : { "Ref" : "AWS::StackName" } },
      { "Key" : "Network", "Value" : "Public" }
    ]
  }
},

This entry waits for the VPC and InternetGateway to finish being provisioned and then are both referenced by their specific names to create the link between the two.

"GatewayToInternet" : {
   "DependsOn" : ["VPC", "InternetGateway"],
   "Type" : "AWS::EC2::VPCGatewayAttachment",
   "Properties" : {
     "VpcId" : { "Ref" : "VPC" },
     "InternetGatewayId" : { "Ref" : "InternetGateway" }
   }
},

Route tables in the VPC do just that. They route traffic. Here we create a public route table that is assigned via the VpcId property to the VPC we created before.

"PublicRouteTable" : {
  "DependsOn" : ["VPC"],
  "Type" : "AWS::EC2::RouteTable",
  "Properties" : {
    "VpcId" : { "Ref" : "VPC" },
    "Tags" : [
      { "Key" : "Application", "Value" : { "Ref" : "AWS::StackName" } },
      { "Key" : "Network", "Value" : "Public" }
    ]
  }
},

Next we add a route to 0.0.0.0/0 (Everything) that we then add to the route table created before and use the Internet Gateway from before as the GatewayId. This route waits for PublicRouteTable and InternetGateway to finish provisioning.

"PublicRoute" : {
  "DependsOn" : ["PublicRouteTable", "InternetGateway"],
  "Type" : "AWS::EC2::Route",
  "Properties" : {
    "RouteTableId" : { "Ref" : "PublicRouteTable" },
    "DestinationCidrBlock" : "0.0.0.0/0",
    "GatewayId" : { "Ref" : "InternetGateway" }
  }
},

We then create two route table associations. Both wait for their respective subnets to finish being created as well as the public route table. We then reference both the SubnetId and RouteTableId in these associations.

"PublicSubnet0RouteTableAssociation" : {
  "DependsOn" : ["PublicSubnet0", "PublicRouteTable"],
  "Type" : "AWS::EC2::SubnetRouteTableAssociation",
  "Properties" : {
    "SubnetId" : { "Ref" : "PublicSubnet0" },
    "RouteTableId" : { "Ref" : "PublicRouteTable" }
  }
},
"PublicSubnet1RouteTableAssociation" : {
  "DependsOn" : ["PublicSubnet1", "PublicRouteTable"],
  "Type" : "AWS::EC2::SubnetRouteTableAssociation",
  "Properties" : {
    "SubnetId" : { "Ref" : "PublicSubnet1" },
    "RouteTableId" : { "Ref" : "PublicRouteTable" }
  }
},

I copied this from a thread on stack exchange but made a minor change in the policy document that does more then just allow access to S3. This also allows for route53 access which will be explained later. Technically AWS would prefer you provide the least amount of access to an instance. This could be improved by only allowing this role to access the specific bucket and route53 zone needed for this VPN. You will see under the action for the S3RolePolicies section where s3: and route53: are setup. Finally we assign the role to the InstanceProfile which will be added to the launch config later.

"S3AccessRole" : {
  "Type"  : "AWS::IAM::Role",
  "Properties" : {
      "AssumeRolePolicyDocument" : {
          "Statement" : [ {
              "Effect" : "Allow",
              "Principal" : {
                  "Service" : [ "ec2.amazonaws.com" ]
             },
            "Action" : [ "sts:AssumeRole" ]
          } ]
      },
      "Path" : "/"
  }
},
"S3RolePolicies" : {
    "Type" : "AWS::IAM::Policy",
    "Properties" : {
        "PolicyName" : "s3route53access",
        "PolicyDocument" : {
            "Statement" : [ {
                "Effect" : "Allow",
                "Action"   : [
                  "s3:*",
                  "route53:*"
                  ],
                "Resource" : "*"
            }]
        },
        "Roles" : [ { "Ref" : "S3AccessRole" } ]
    }
},
"S3InstanceProfile" : {
    "Type" : "AWS::IAM::InstanceProfile",
    "Properties" : {
        "Path" : "/",
        "Roles" : [ { "Ref" : "S3AccessRole" } ]
    }
},

Here we setup the security group for each instance. This allows port 1194 over udp for OpenVPN as well as 22 access from the CidrIp setup earier. I allowed all in my setup because I wanted to be able to VPN and/or ssh to the instance from anywhere. You will also notice that the VpcId is set by a reference to the VPC above.

"WebServerSecurityGroup" : {
  "Type" : "AWS::EC2::SecurityGroup",
  "Properties" : {
    "GroupDescription" : "Enable VPN access via port 1194 + SSH access",
    "VpcId" : { "Ref" : "VPC" },
    "SecurityGroupIngress" : [
      {"IpProtocol" : "udp", "FromPort" : "1194", "ToPort" : "1194", "CidrIp" : { "Ref" : "SSHLocation"}},
      {"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"}}
    ]
  }
},

Next we setup the WebServerGroup. This is an autoscaling group that using the lanch config defind next. It depends on the two public subnets to be finished before it can start. Under our properties section we define a minimum size (1), a maximum size (5), desired capacity (1) defined by the WebServerCapacity from before as well as VPCZoneIdentifier which references the subnets from before. Tags are then setup in the properties. I am tagging these instances with the name VPNNNS so I know which instance is the VPN instance later when looking at the AWS console. We setup a creation policy and set a 15 minute timeout for it (fairly default) as well as an update policy that sets up rolling updates against 1 instance at a time if needed.

"WebServerGroup" : {
  "Type" : "AWS::AutoScaling::AutoScalingGroup",
  "DependsOn" : ["PublicSubnet0", "PublicSubnet1"],
  "Properties" : {
    "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
    "MinSize" : "1",
    "MaxSize" : "5",
    "DesiredCapacity" : { "Ref" : "WebServerCapacity" },
    "Tags" : [ { "Key" : "Name", "Value" : "VPNNNS", "PropagateAtLaunch" : true } ],
    "VPCZoneIdentifier" : [{ "Ref": "PublicSubnet0" }, { "Ref": "PublicSubnet1" }]
  },
  "CreationPolicy" : {
    "ResourceSignal" : {
      "Timeout" : "PT15M"
    }
  },
  "UpdatePolicy": {
    "AutoScalingRollingUpdate": {
      "MinInstancesInService": "1",
      "MaxBatchSize": "1",
      "PauseTime" : "PT15M",
      "WaitOnResourceSignals": "true"
    }
  }
},

In this section we setup the LaunchConfig. This is probably the most important part of the CF template and the easiest to mess up. So take care to read thought everything. We start off by defining this as an "AWS::AutoScaling::LaunchConfiguration".
Under metadata we setup configSets. These config sets are commands that the instances will execute when starting up.
We have two "install_cfn" and "install_openvpn". "install_cfn" is the first and sets up two config files. These config files inform the instance which CF stack they are apart of as well as region as well as informs it to also do the 2nd configSet "openvpn_install". The LaunchConfig then makes sure the cfn-hup service is running to finish the install.

"LaunchConfig": {
  "Type" : "AWS::AutoScaling::LaunchConfiguration",
  "Metadata" : {
    "AWS::CloudFormation::Init" : {
      "configSets" : {
        "openvpn_install" : ["install_cfn", "install_openvpn" ]
      },
      "install_cfn" : {
        "files": {
          "/etc/cfn/cfn-hup.conf": {
            "content": { "Fn::Join": [ "", [
              "[main]\n",
              "stack=", { "Ref": "AWS::StackId" }, "\n",
              "region=", { "Ref": "AWS::Region" }, "\n"
            ]]},
            "mode"  : "000400",
            "owner" : "root",
            "group" : "root"
          },
          "/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
            "content": { "Fn::Join": [ "", [
              "[cfn-auto-reloader-hook]\n",
              "triggers=post.update\n",
              "path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init\n",
              "action=/opt/aws/bin/cfn-init -v ",
                      "         --stack ", { "Ref" : "AWS::StackName" },
                      "         --resource LaunchConfig ",
                      "         --configsets openvpn_install ",
                      "         --region ", { "Ref" : "AWS::Region" }, "\n"
            ]]},
            "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"]}
          }
        }
      },

Next we have the "install_openvpn" configSet. This set starts off by installing the packages required for openvpn. Once installed it then runs the commands to download the openvpn credentials and config file I previously setup and pushed to an s3 bucket. I use the "aws s3 sync" command to sync that folder to the "/etc/openvpn" folder.

      "install_openvpn" : {
        "packages" : {
          "yum" : {
            "openvpn" : []
          }
        },
        "commands" : {
          "01_download_openvpn_stuff": {
            "command" : "aws s3 sync s3://vpn-bucket/ ./",
            "cwd" : "/etc/openvpn"
          },

We then setup iptables and the sysctl.conf file. This is done to allow the VPN clients to be forwarded to the internet properly.
We use sed for the modification of the sysctl.conf file to allow for it to be edited in place. We then run "sysctl -p" to reload it.

          "02_setup_iptables": {
            "command" : "iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE; sed -i 's/net.ipv4.ip_forward = 0/net.ipv4.ip_forward = 1/' /etc/sysctl.conf; sed -i 's/net.bridge.bridge-nf-call-ip6tables = 0//' /etc/sysctl.conf; sed -i 's/net.bridge.bridge-nf-call-iptables = 0//' /etc/sysctl.conf; sed -i 's/net.bridge.bridge-nf-call-arptables = 0//' /etc/sysctl.conf; sysctl -p"
          },

Now that we are closing out the CF template shortly we can restart OpenVPN.

          "03_restart_openvpn" : {
            "command" : "service openvpn restart"
          },

And update our DNS address with the public IP of the instance. We get the public ip of the instance by curl from the instance metadata service. We first download the cli53 utility from AWS which is just a simple route53 update utility. We make it executable and then update the route53 entry. This is a fairly simple bash command that sets up a variable called public_ip which we use to populate the cli53 command.

          "04_update_dns" : {
            "command" : "wget -O cli53 https://github.com/barnybug/cli53/releases/download/0.6.9/cli53-linux-amd64; chmod +x ./cli53; public_ip=$(curl http://169.254.169.254/latest/meta-data/public-ipv4); ./cli53 rrcreate --replace domain.com 'vpn-hostname 60 A '$public_ip"
          }
        },
        "services" : {
          "sysvinit" : {
            "openvpn" : { "enabled" : "true", "ensureRunning" : "true" }
          }
        }
      }
    }
  },

Finally we update the Properties section of the Launch Config. This sets the ImageId which is figured out via the Mappings setup previously. We tell it to associate a public IP address. We assign it the IamInstanceProfile setup previously. We tell it to use the InstanceType from eariler as will as the WebServerSecurityGroup and KeyName. We use a default instance UserData that signals back to the CF service that the instance is done being provisioned which will then signal to us that we are good to go!

  "Properties": {
    "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
                      { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
    "AssociatePublicIpAddress" : true,
    "IamInstanceProfile" : { "Ref" : "S3InstanceProfile" },
    "InstanceType"   : { "Ref" : "InstanceType" },
    "SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ],
    "KeyName"        : { "Ref" : "KeyName" },
    "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
                   "#!/bin/bash -xe\n",
                   "yum update -y aws-cfn-bootstrap\n",
                   "/opt/aws/bin/cfn-init -v ",
                   "         --stack ", { "Ref" : "AWS::StackName" },
                   "         --resource LaunchConfig ",
                   "         --configsets openvpn_install ",
                   "         --region ", { "Ref" : "AWS::Region" }, "\n",
                   "/opt/aws/bin/cfn-signal -e $? ",
                   "         --stack ", { "Ref" : "AWS::StackName" },
                   "         --resource WebServerGroup ",
                   "         --region ", { "Ref" : "AWS::Region" }, "\n"
    ]]}}
  }
}
}

The VPN needs a server.conf to setup the server. I copied the following from a few locations:

port 1194 # Sets the port to 1194
proto udp # Sets the protocol to udp
dev tun # Sets the device to tun
ca ca.crt # Tells it to use the ca cert named ca.crt
cert server.crt # Tells it to use the cert named server.crt
key server.key  # This file should be kept secret, tells it to use the key cert named server.key
dh dh2048.pem # Tells it to use the dh key named dh2048.pem
server 10.8.0.0 255.255.255.0 # Sets the network cidr range
push "redirect-gateway def1 bypass-dhcp" # Tells it to make the clients pass all traffic via the VPN
push "dhcp-option DNS 8.8.8.8" # Sets the DNS to googles public dns
push "dhcp-option DNS 8.8.4.4"
keepalive 10 120  # Keep alive settings
cipher AES-128-CBC   # AES 128 bit encryption
comp-lzo  # Use compression
user nobody # Run as nobody
group nobody # In the nobody group
persist-key # Set persistent key
persist-tun # And persistent tun

Lastly to create the certs I used easy-rsa and this link to create the certs. I'll end it here because the rest is easy to follow.