Configuring a resilient and redundant OpenVPN service

It's been awhile since I did an article, but here it goes! Today I bring you an article about OpenVPN.

Why am I writing about OpenVPN? Because it's an easy and cheap way to connect to your AWS resources from anywhere without exposing yourself to the whole Internet.

I'm going to walk through using CloudFormation to bootstrap OpenVPN, and how to configure OpenVPN with two different authentication models - local pam authentication and ldap authentication.

Building a Service with CloudFormation

If you've read my other CloudFormation articles, then you'll know how this goes. If you haven't I heavily suggest you take a look!

Here's the base template before we do much:

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "This CloudFormation template provisions a VPN service with an EC2 instance configured with OpenVPN in each subnet, and each instance is added to an ELB. An appropriate DNS ALIAS or CNAME record should be configured to point to the ELB.",
    "Parameters" : {
    },
    "Conditions" : {
    },
    "Resources" : {
    },
    "Outputs" : {
    }
}

For starters we're going to need to add two security groups - one for the EC2 instances and one for the Elastic Load Balancer. The basic structure of a security group looks like this:

{
  "Type" : "AWS::EC2::SecurityGroup",
  "Properties" : {
     "GroupDescription" : String,
     "SecurityGroupEgress" : [ Security Group Rule, ... ],
     "SecurityGroupIngress" : [ Security Group Rule, ... ],
     "Tags" :  [ Resource Tag, ... ],
     "VpcId" : String
  }
}

You can see a more in-depth example here.

In our case this is what our template looks like with the addition of the two security groups and a parameter (VPC ID):

{
...
    "Parameters" : {
        "VpcID" : {
            "Description" : "The IP Address space used by this VPC. Changing the VPC-id issues a replacement of the stack.",
            "Type" : "String"
        }
    },
...
    "Resources" : {
        "ElbSecurityGroup" : {
            "Type" : "AWS::EC2::SecurityGroup",
            "Properties" :
            {
                "GroupDescription" : { "Ref" : "AWS::StackName"},
                "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "1194", "ToPort" : "1194", "CidrIp" : "0.0.0.0/0"} ],
                "VpcId" : { "Ref" : "VpcID"},
                "Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"} } ]
            }
        },
        "Ec2InstanceSecurityGroup" : {
            "Type" : "AWS::EC2::SecurityGroup",
            "Properties" :
            {
                "GroupDescription" : { "Ref" : "AWS::StackName"},
                "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "1194", "ToPort" : "1194", "SourceSecurityGroupId" : {
                    "Fn::GetAtt": [
                        "ElbSecurityGroup",
                        "GroupId"
                    ]
                }} ],
                "VpcId" : { "Ref" : "VpcID"},
                "Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"} } ]
            }
        }
    }

Now to build out a load balancer:

...
        "Subnet1ID" : {
            "Description" : "The IP Address space used by subnet 1. Changing the subnet-id issues a replacement of the stack.",
            "Type" : "String"
        },
        "Subnet2ID" : {
            "Description" : "The IP Address space used by subnet 2. Changing the subnet-id issues a replacement of the stack.",
            "Type" : "String"
        },
        "Subnet3ID" : {
            "Description" : "The IP Address space used by subnet 3. Changing the subnet-id issues a replacement of the stack.",
            "Type" : "String"
        }
...
        "LoadBalancer" : {
            "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
            "Properties" : {
                "CrossZone" : "true",
                "SecurityGroups" : [ { "Ref" : "ElbSecurityGroup" } ],
                "Subnets" : [{ "Ref" : "Subnet1ID"},{ "Ref" : "Subnet2ID"},{ "Ref" : "Subnet3ID"}],
                "Listeners" : [ { "LoadBalancerPort" : "1194", "InstancePort" : "1194", "Protocol" : "TCP" } ],
                "HealthCheck" : {
                    "Target" : "TCP:1194",
                    "HealthyThreshold" : "3",
                    "UnhealthyThreshold" : "2",
                    "Interval" : "10",
                    "Timeout" : "5"
                },
                "Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"} } ]
            }
        }
...

We need an IAM role for the server to access S3:

...
        "InstanceRole":{
            "Type":"AWS::IAM::Role",
            "Properties":{
                "AssumeRolePolicyDocument":{
                    "Statement":[
                        {
                            "Effect":"Allow",
                            "Principal":{
                                "Service":[
                                    "ec2.amazonaws.com"
                                ]
                            },
                            "Action":[
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Path":"/"
            }
        },
        "RolePolicy1":{
            "Type":"AWS::IAM::Policy",
            "Properties":{
                "PolicyName":"S3Download",
                "PolicyDocument":{
                    "Statement":[
                        {
                            "Action":[
                                "s3:ListBucket"
                            ],
                            "Effect":"Allow",
                            "Resource":[
                                { "Fn::Join" : [ "", ["arn:aws:s3:::", {"Ref":"S3Bucket"} ] ] } }
                            ]
                        },
                        {
                            "Action":[
                                "s3:Get*",
                                "s3:List*"
                            ],
                            "Effect":"Allow",
                            "Resource": [
                                { "Fn::Join" : [ "", ["arn:aws:s3:::", {"Ref":"S3Bucket"},"/*" ] ] } }
                            ]
                        }
                    ]
                },
                "Roles":[
                    {
                        "Ref":"InstanceRole"
                    }
                ]
            }
        },
        "InstanceProfile":{
            "Type":"AWS::IAM::InstanceProfile",
            "Properties":{
                "Path":"/",
                "Roles":[
                    {
                        "Ref":"InstanceRole"
                    }
                ]
            }
        }
...

This is the complex stage - building a launch config. I think it's fairly obvious what's going on here though. You can read more on launch configs here. Anyways, here it is:

...
        "LaunchConfig" : {
            "Type" : "AWS::AutoScaling::LaunchConfiguration",
            "Metadata" : {
                "Comment" : "Installs OpenVPN server.",
                "AWS::CloudFormation::Authentication": {
                    "S3AccessCreds": {
                        "type": "S3",
                        "roleName": { "Ref" : "InstanceRole"},
                        "buckets" : [{"Ref":"S3Bucket"}]
                    }
                },
                "AWS::CloudFormation::Init" : {
                    "configSets" : {
                        "ascending" : [ "initial", "repos", "config" ]
                    },
                    "initial" : {
                        "commands" : {
                            "sudoersttypatch" : {
                                "command" : "sed -i.bak 's/requiretty/!requiretty/g' /etc/sudoers"
                            }
                        },
                        "packages" : {
                            "yum" : {
                                "gcc"                   : [],
                                "make"                  : [],
                                "tcpdump"               : [],
                                "jq"                    : [],
                                "telnet"                : []
                            }
                        }
                    },
                    "repos" : {
                        "commands" : {
                            "enableepel" : {
                                "command" : "yum-config-manager --enable epel;"
                            }
                        }
                    },
                    "config" : {
                        "commands" : {
                            "ipforwarding" : {
                                "command" : "echo '1' > /proc/sys/net/ipv4/ip_forward;sed -i.bak s/'net.ipv4.ip_forward = 0'/'net.ipv4.ip_forward = 1'/g /etc/sysctl.conf; sysctl -e -p;"
                            },
                            "iptables" : {
                                "command" : "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;iptables -I FORWARD -i tun0 -o eth0 -j ACCEPT;iptables -I FORWARD -i eth0 -o tun0 -j ACCEPT;/sbin/service iptables save;"
                            },
                            "ldapconf" : {
                                "command" : "echo 'TLS_REQCERT never' > /etc/openldap/ldap.conf"
                            }
                        },
                        "packages" : {
                            "yum" : {
                                "openvpn"            : [],
                                "openvpn-auth-ldap"  : [],
                                "easy-rsa"           : [],
                                "tcpdump"            : [],
                                "jq"                 : [],
                                "telnet"             : []
                            }
                        },
                        "sources" : {
                            "/etc/openvpn" : { "Fn::Join" : ["", ["https://s3-",{ "Ref" : "AWS::Region" },".amazonaws.com/",{"Ref":"S3Bucket"},"/",{ "Ref" : "Key" },"-openvpn.zip"]]}
                        },
                        "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 ",
                                    "         --region ", { "Ref" : "AWS::Region" }, "\n",
                                    "runas=root\n"
                                ]]}
                            }
                        },
                        "services" : {
                            "sysvinit" : {
                                "cfn-hup" : { "enabled" : "true", "ensureRunning" : "true", "files" : ["/etc/cfn/cfn-hup.conf", "/etc/cfn/hooks.d/cfn-auto-reloader.conf"]},
                                "openvpn" : { "enabled" : "true", "ensureRunning" : "true"}
                            }
                        }
                    }
                }
            },
            "Properties" : {
                "IamInstanceProfile":{
                    "Ref":"InstanceProfile"
                },
                "KeyName" : {"Ref" : "SSHKeyPair"},
                "ImageId" :  {"Ref" : "InstanceAMI" },
                "SecurityGroups" : [ { "Ref" : "Ec2InstanceSecurityGroup" }],
                "InstanceType" : { "Ref" : "InstanceType" },
                "AssociatePublicIpAddress" : true,
                "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 ",
                    "         --configset ascending",
                    "         --region ", { "Ref" : "AWS::Region" }, "\n",
                    "/opt/aws/bin/cfn-signal -e $? ",
                    "         --stack ", { "Ref" : "AWS::StackName" },
                    "         --resource LaunchConfig ",
                    "         --region ", { "Ref" : "AWS::Region" }, "\n"
                ]]}}
            }
        }
...

Now the Auto Scaling group:

...
        "AutoScalingGroup" : {
            "Type" : "AWS::AutoScaling::AutoScalingGroup",
            "Properties" : {
                "AvailabilityZones" : [{ "Ref" : "Subnet1AZ"},{ "Ref" : "Subnet2AZ"},{ "Ref" : "Subnet3AZ"}],
                "VPCZoneIdentifier" : [{ "Ref" : "Subnet1ID"},{ "Ref" : "Subnet2ID"},{ "Ref" : "Subnet3ID"}],
                "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
                "MinSize" : {"Ref":"MinSize"},
                "MaxSize" : {"Ref":"MaxSize"},
                "LoadBalancerNames" : [ { "Ref" : "LoadBalancer" } ],
                "Tags" : [
                    { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"}, "PropagateAtLaunch" : true }
                ]
            },
            "CreationPolicy" : {
                "ResourceSignal" : {
                    "Count" : "1",
                    "Timeout" : "PT15M"
                }
            },
            "UpdatePolicy": {
                "AutoScalingRollingUpdate": {
                    "MinInstancesInService": "1",
                    "MaxBatchSize": "1",
                    "PauseTime" : "PT5M",
                    "WaitOnResourceSignals": "true"
                }
            }
        }
...

And here's the completed template:

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "This CloudFormation template provisions a VPN service with an EC2 instance configured with OpenVPN in each subnet, and each instance is added to an ELB. An appropriate DNS ALIAS or CNAME record should be configured to point to the ELB.",
    "Parameters" : {
        "InstanceAMI" : {
            "Description" : "The EC2 AMI id to use for the OpenVPN service. Expects an Amazon Linux box, but may work with others.",
            "Type" : "String",
            "Default" : "ami-b5a7ea85"
        },
        "MinSize" : {
            "Description" : "The minimum size of the VPN auto scaling group. Can be used for horizontal scaling.",
            "Type" : "String",
            "Default" : "1"
        },
        "MaxSize" : {
            "Description" : "The maximum size of the VPN auto scaling group. Can be used for horizontal scaling. Always keep +1 over the MinSize.",
            "Type" : "String",
            "Default" : "2"
        },
        "InstanceType" : {
            "Description" : "EC2 instance type. If having network speed / consistency issues, then increase this value.",
            "Type" : "String",
            "Default" : "c3.xlarge",
            "AllowedValues" : [ "t2.micro", "t2.medium", "m3.large", "m3.xlarge", "m3.2xlarge", "c1.medium", "c1.xlarge", "c3.large", "c3.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge"],
            "ConstraintDescription" : "must be a valid EC2 instance type."
        },
        "SSHKeyPair" : {
            "Description" : "The SSH Key Pair to use with the auto scaling group.",
            "Type" : "AWS::EC2::KeyPair::KeyName"
        },
        "S3Bucket" : {
            "Description" : "The S3 bucket to pull the OpenVPN configuration from.",
            "Type" : "String"
        },
        "Key" : {
            "Description" : "Region / Key word, used to select which OpenVPN configuration to use.",
            "Type" : "String"
        },
        "VpcID" : {
            "Description" : "The IP Address space used by this VPC. Changing the VPC-id issues a replacement of the stack.",
            "Type" : "String"
        },
        "Subnet1ID" : {
            "Description" : "The IP Address space used by subnet 1. Changing the subnet-id issues a replacement of the stack.",
            "Type" : "String"
        },
        "Subnet2ID" : {
            "Description" : "The IP Address space used by subnet 2. Changing the subnet-id issues a replacement of the stack.",
            "Type" : "String"
        },
        "Subnet3ID" : {
            "Description" : "The IP Address space used by subnet 3. Changing the subnet-id issues a replacement of the stack.",
            "Type" : "String"
        },
        "Subnet1AZ" : {
            "Description" : "The availability zone used by subnet 1. Changing the AZ issues a replacement of the stack. Must match the AZ the subnet is actually in.",
            "Type" : "String"
        },
        "Subnet2AZ" : {
            "Description" : "The availability zone used by subnet 2. Changing the AZ issues a replacement of the stack. Must match the AZ the subnet is actually in.",
            "Type" : "String"
        },
        "Subnet3AZ" : {
            "Description" : "The availability zone used by subnet 3. Changing the AZ issues a replacement of the stack. Must match the AZ the subnet is actually in.",
            "Type" : "String"
        }
    },
    "Conditions" : {
    },
    "Resources" : {
        "ElbSecurityGroup" : {
            "Type" : "AWS::EC2::SecurityGroup",
            "Properties" :
            {
                "GroupDescription" : { "Ref" : "AWS::StackName"},
                "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "1194", "ToPort" : "1194", "CidrIp" : "0.0.0.0/0"} ],
                "VpcId" : { "Ref" : "VpcID"},
                "Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"} } ]
            }
        },
        "Ec2InstanceSecurityGroup" : {
            "Type" : "AWS::EC2::SecurityGroup",
            "Properties" :
            {
                "GroupDescription" : { "Ref" : "AWS::StackName"},
                "SecurityGroupIngress" : [ { "IpProtocol" : "tcp", "FromPort" : "1194", "ToPort" : "1194", "SourceSecurityGroupId" : {
                    "Fn::GetAtt": [
                        "ElbSecurityGroup",
                        "GroupId"
                    ]
                }} ],
                "VpcId" : { "Ref" : "VpcID"},
                "Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"} } ]
            }
        },
        "LoadBalancer" : {
            "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
            "Properties" : {
                "CrossZone" : "true",
                "SecurityGroups" : [ { "Ref" : "ElbSecurityGroup" } ],
                "Subnets" : [{ "Ref" : "Subnet1ID"},{ "Ref" : "Subnet2ID"},{ "Ref" : "Subnet3ID"}],
                "Listeners" : [ { "LoadBalancerPort" : "1194", "InstancePort" : "1194", "Protocol" : "TCP" } ],
                "HealthCheck" : {
                    "Target" : "TCP:1194",
                    "HealthyThreshold" : "3",
                    "UnhealthyThreshold" : "2",
                    "Interval" : "10",
                    "Timeout" : "5"
                },
                "Tags" : [ { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"} } ]
            }
        },
        "InstanceRole":{
            "Type":"AWS::IAM::Role",
            "Properties":{
                "AssumeRolePolicyDocument":{
                    "Statement":[
                        {
                            "Effect":"Allow",
                            "Principal":{
                                "Service":[
                                    "ec2.amazonaws.com"
                                ]
                            },
                            "Action":[
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Path":"/"
            }
        },
        "RolePolicy1":{
            "Type":"AWS::IAM::Policy",
            "Properties":{
                "PolicyName":"S3Download",
                "PolicyDocument":{
                    "Statement":[
                        {
                            "Action":[
                                "s3:ListBucket"
                            ],
                            "Effect":"Allow",
                            "Resource":[
                                { "Fn::Join" : [ "", ["arn:aws:s3:::", {"Ref":"S3Bucket"} ] ] } }
                            ]
                        },
                        {
                            "Action":[
                                "s3:Get*",
                                "s3:List*"
                            ],
                            "Effect":"Allow",
                            "Resource": [
                                { "Fn::Join" : [ "", ["arn:aws:s3:::", {"Ref":"S3Bucket"},"/*" ] ] } }
                            ]
                        }
                    ]
                },
                "Roles":[
                    {
                        "Ref":"InstanceRole"
                    }
                ]
            }
        },
        "InstanceProfile":{
            "Type":"AWS::IAM::InstanceProfile",
            "Properties":{
                "Path":"/",
                "Roles":[
                    {
                        "Ref":"InstanceRole"
                    }
                ]
            }
        },
        "AutoScalingGroup" : {
            "Type" : "AWS::AutoScaling::AutoScalingGroup",
            "Properties" : {
                "AvailabilityZones" : [{ "Ref" : "Subnet1AZ"},{ "Ref" : "Subnet2AZ"},{ "Ref" : "Subnet3AZ"}],
                "VPCZoneIdentifier" : [{ "Ref" : "Subnet1ID"},{ "Ref" : "Subnet2ID"},{ "Ref" : "Subnet3ID"}],
                "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
                "MinSize" : {"Ref":"MinSize"},
                "MaxSize" : {"Ref":"MaxSize"},
                "LoadBalancerNames" : [ { "Ref" : "LoadBalancer" } ],
                "Tags" : [
                    { "Key" : "Name", "Value" : { "Ref" : "AWS::StackName"}, "PropagateAtLaunch" : true }
                ]
            },
            "CreationPolicy" : {
                "ResourceSignal" : {
                    "Count" : "1",
                    "Timeout" : "PT15M"
                }
            },
            "UpdatePolicy": {
                "AutoScalingRollingUpdate": {
                    "MinInstancesInService": "1",
                    "MaxBatchSize": "1",
                    "PauseTime" : "PT5M",
                    "WaitOnResourceSignals": "true"
                }
            }
        },
        "LaunchConfig" : {
            "Type" : "AWS::AutoScaling::LaunchConfiguration",
            "Metadata" : {
                "Comment" : "Installs OpenVPN server.",
                "AWS::CloudFormation::Authentication": {
                    "S3AccessCreds": {
                        "type": "S3",
                        "roleName": { "Ref" : "InstanceRole"},
                        "buckets" : [{"Ref":"S3Bucket"}]
                    }
                },
                "AWS::CloudFormation::Init" : {
                    "configSets" : {
                        "ascending" : [ "initial", "repos", "config" ]
                    },
                    "initial" : {
                        "commands" : {
                            "sudoersttypatch" : {
                                "command" : "sed -i.bak 's/requiretty/!requiretty/g' /etc/sudoers"
                            }
                        },
                        "packages" : {
                            "yum" : {
                                "gcc"                   : [],
                                "make"                  : [],
                                "tcpdump"               : [],
                                "jq"                    : [],
                                "telnet"                : []
                            }
                        }
                    },
                    "repos" : {
                        "commands" : {
                            "enableepel" : {
                                "command" : "yum-config-manager --enable epel;"
                            }
                        }
                    },
                    "config" : {
                        "commands" : {
                            "ipforwarding" : {
                                "command" : "echo '1' > /proc/sys/net/ipv4/ip_forward;sed -i.bak s/'net.ipv4.ip_forward = 0'/'net.ipv4.ip_forward = 1'/g /etc/sysctl.conf; sysctl -e -p;"
                            },
                            "iptables" : {
                                "command" : "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;iptables -I FORWARD -i tun0 -o eth0 -j ACCEPT;iptables -I FORWARD -i eth0 -o tun0 -j ACCEPT;/sbin/service iptables save;"
                            },
                            "ldapconf" : {
                                "command" : "echo 'TLS_REQCERT never' > /etc/openldap/ldap.conf"
                            }
                        },
                        "packages" : {
                            "yum" : {
                                "openvpn"            : [],
                                "openvpn-auth-ldap"  : [],
                                "easy-rsa"           : [],
                                "tcpdump"            : [],
                                "jq"                 : [],
                                "telnet"             : []
                            }
                        },
                        "sources" : {
                            "/etc/openvpn" : { "Fn::Join" : ["", ["https://s3-",{ "Ref" : "AWS::Region" },".amazonaws.com/",{"Ref":"S3Bucket"},"/",{ "Ref" : "Key" },"-openvpn.zip"]]}
                        },
                        "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 ",
                                    "         --region ", { "Ref" : "AWS::Region" }, "\n",
                                    "runas=root\n"
                                ]]}
                            }
                        },
                        "services" : {
                            "sysvinit" : {
                                "cfn-hup" : { "enabled" : "true", "ensureRunning" : "true", "files" : ["/etc/cfn/cfn-hup.conf", "/etc/cfn/hooks.d/cfn-auto-reloader.conf"]},
                                "openvpn" : { "enabled" : "true", "ensureRunning" : "true"}
                            }
                        }
                    }
                }
            },
            "Properties" : {
                "IamInstanceProfile":{
                    "Ref":"InstanceProfile"
                },
                "KeyName" : {"Ref" : "SSHKeyPair"},
                "ImageId" :  {"Ref" : "InstanceAMI" },
                "SecurityGroups" : [ { "Ref" : "Ec2InstanceSecurityGroup" }],
                "InstanceType" : { "Ref" : "InstanceType" },
                "AssociatePublicIpAddress" : true,
                "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 ",
                    "         --configset ascending",
                    "         --region ", { "Ref" : "AWS::Region" }, "\n",
                    "/opt/aws/bin/cfn-signal -e $? ",
                    "         --stack ", { "Ref" : "AWS::StackName" },
                    "         --resource LaunchConfig ",
                    "         --region ", { "Ref" : "AWS::Region" }, "\n"
                ]]}}
            }
        }
    },
    "Outputs" : {
        "ELB" : {
            "Description" : "VPN ELB Endpoint.",
            "Value" : {"Fn::GetAtt" : [ "LoadBalancer" , "DNSName" ]}
        }
    }
}

Building a Valid OpenVPN Configuration with Local Accounts

I'm not going to go into super depth here about the different options and values and their effects. Suffice it to say that this configuration exposes a basic VPN tunnel with username and password authentication with reasonable settings. It's currently missing it's certificates, but we'll fill those in in just a moment.

port 1194
proto tcp
dev tun

<ca> —–BEGIN CERTIFICATE—– —–END CERTIFICATE—– </ca> <cert> —–BEGIN CERTIFICATE—– —–END CERTIFICATE—– </cert> <key> —–BEGIN PRIVATE KEY—– —–END PRIVATE KEY—– </key> <dh> —–BEGIN DH PARAMETERS—– —–END DH PARAMETERS—– </dh>

server 192.168.248.0 255.255.255.0

AWS Route

push “route 10.248.0.0 255.248.0.0”

ifconfig-pool-persist ipp.txt keepalive 10 120 comp-lzo max-clients 100 user root group root persist-key persist-tun status /var/log/openvpn-status.log log-append /var/log/openvpn.log verb 6 mute 5 client-cert-not-required plugin /usr/lib64/openvpn/plugin/lib/openvpn-auth-pam.so login reneg-sec 0 mssfix mute-replay-warnings

Of particular interest is this line: plugin /usr/lib64/openvpn/plugin/lib/openvpn-auth-pam.so login

That’s the line that specifies to use local pam authentication. In the next section we’ll visit switching this to LDAP.

Anyways, to generate the certificates above:

  1. yum install -y easy-rsa
  2. cd /usr/share/easy-rsa/2.0/ (note that the version number may differ on your system)
  3. source ./vars
  4. ./clean-all
  5. ./build-ca (Follow the prompts)
  6. ./build-key-server server (Follow the prompts)
  7. ./build-dh
  8. cd keys
  9. ls

You’ve now generated the files you’ll need. Copy the contents into the correct location in the config like so:

<ca></ca>      <— /usr/share/easy-rsa/2.0/keys/ca.crt
<cert></cert>  <— /usr/share/easy-rsa/2.0/keys/server.crt
<key></key>    <— /usr/share/easy-rsa/2.0/keys/server.key
<dh></dh>      <— /usr/share/easy-rsa/2.0/keys/dh2048.pem

You can zip the file up and upload it to a bucket in S3 now.

Extending OpenVPN with LDAP-based Authentication

Changing to use the LDAP-based authentication is fairly straight forward and only requires an additional file and a line change.

The line we noted previously that was of particular interest is the line we need to change: plugin /usr/lib64/openvpn/plugin/lib/openvpn-auth-pam.so login to plugin /usr/lib64/openvpn/plugin/lib/openvpn-auth-ldap.so “auth/ldap.conf”.

Next we need to add an LDAP configuation file to ./auth/ldap.conf. The basic file looks like:

<LDAP>
URL             ldaps://dc.corp.example.com:636
Timeout         120
BindDN          “cn=vpn,cn=Users,dc=corp,dc=example,dc=com”
Password        ""
</LDAP>
<Authorization>
BaseDN          “cn=Users,dc=corp,dc=example,dc=com”
SearchFilter    “(sAMAccountName=%u)”
</Authorization>

You can limit by group membership pretty easily by adding a section to the authorization:

        RequireGroup    true
<Group>
BaseDN          “ou=groups,cn=Users,dc=corp,dc=example,dc=com”
SearchFilter    “(|(cn=vpn-access)(cn=oncall))”
MemberAttribute “member”
</Group>

Create Stack!

Once you’ve gotten a working template and uploaded a valid VPN configuration to S3 you can run the create stack process to get a working elastic load balancer endpoint.

Client Configuration

With a working endpoint and the CA certificate from earlier we can generate a valid client configuration like so:

################################################################################

Configure port, protocol, and tunnel type. See man openvpn for details.

################################################################################ client dev tun proto tcp remote YOUR_DNS_ENDPOINT_HERE 1194

################################################################################

Configure the certificate authority certificate.

################################################################################ —–BEGIN CERTIFICATE—– —–END CERTIFICATE—–

################################################################################

Configure the openvpn client tunnel settings.

################################################################################ resolv-retry infinite ns-cert-type server nobind persist-key persist-tun mute-replay-warnings auth-user-pass comp-lzo verb 5 mute 5 keepalive 5 30 mssfix reneg-sec 0

Download the source files

You can get all the files from this article from this repo.