OpenVPN on AWS VPC with LDAP

I recently just fought my way through getting OpenVPN community edition running on our AWS VPC environment and wanted to share so that other can learn. There are a few key take aways and I'm just going to focus on the key elements.

  1. Past experience has shown that you don't want to use 192.168.X.X or 10.0.X.X as your VPN networks. They are frequently used by home routers and having people configure into the VPN with that network conflict isn't worth the effort.
  2. The Ubuntu package openvpn-auth-ldap will coredump, don't use it go via the PAM route.
  3. You need to have separate authentication from your distributed configuration. e.g. Don't assume that the key files are all that you need.

A little credit - Found these posts to be quite helpful, along with about 50 others.

This guide is only a few steps long:

  1. Creating an instance
  2. Configuration OpenVPN
  3. UserData for your instance (boot scripts)

Step 1 - Creating an Instance

Create your VPN instance as a bastion host, where it's on the "public" subnet of your VPC with an IP address assigned. Do not forget that you need an instance that has Source/Dest checks disabled

Note: Configuration Information

  • VPC Cidr is 172.20.0.0/16
  • VPN network will be 172.29.0.0/20
  • VPN public network is 172.20.101.0/20
  • Using a Ubuntu 16.04 image

You need to allow UDP to port 1194 to have access to the server.

Step 1.5 - LDAP Directory

Cut to the chase, I really don't want to manage LDAP directories. This is a great cloud function and have deligated it to Jumpcloud. As of this writing I've used their services for 1 day, but happy so far.

Step 2 - Configure OpenVPN

In general you should follow any one of the many online guides for creating your keys, this is pretty boiler plate. At the end you'll want to have three sets of files:

Server Keys

These will be generated by the guides that you follow for OpenVPN key generation

  • server.crt
  • server.key
  • dh2048.pem

Server Configuration

  • server.conf
  • ldap.conf

server.conf

 1port 1194
 2proto udp
 3dev tun
 4server 172.29.0.0 255.255.240.0
 5push "route 172.20.0.0 255.255.0.0"
 6ca /etc/openvpn/keys/ca.crt
 7cert /etc/openvpn/keys/server.crt
 8key /etc/openvpn/keys/server.key
 9dh /etc/openvpn/keys/dh2048.pem
10tls-version-min 1.2
11tls-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
12cipher AES-256-CBC
13auth SHA512
14ifconfig-pool-persist ipp.txt
15keepalive 10 120
16comp-lzo
17persist-key
18persist-tun
19status openvpn-status.log
20log-append  /var/log/openvpn.log
21verb 3
22max-clients 100
23user nobody
24group nogroup
25plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so openvpn

ldap.conf - using jumpcloud

 1host ldap.jumpcloud.com
 2base ou=Users,o=SECRET,dc=jumpcloud,dc=com
 3binddn uid=ldap-aws,ou=Users,o=SECRET,dc=jumpcloud,dc=com
 4bindpw VERYSECRET
 5scope one
 6timelimit 5
 7bind_timelimit 2
 8bind_policy soft
 9idle_timelimit 6
10
11pam_login_attribute uid
12
13pam_min_uid 1000
14pam_password exop
15
16nss_base_passwd ou=Users,o=SECRET,dc=jumpcloud,dc=com
17nss_base_shadow ou=Users,o=SECRET,dc=jumpcloud,dc=com
18
19ssl start_tls
20tls_checkpeer no

Client Configuration -- Tunnelblick files

  • ca.crt
  • yourorg.crt
  • yourorg.key
  • client.conf

client.conf

 1client
 2dev tun
 3proto udp
 4remote YOUR.DNS.NAME 1194
 5ca ca.crt
 6cert yourorg.crt
 7key yourorg.key
 8tls-version-min 1.2
 9tls-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
10cipher AES-256-CBC
11auth SHA512
12resolv-retry infinite
13auth-retry none
14nobind
15persist-key
16persist-tun
17ns-cert-type server
18comp-lzo
19verb 3
20auth-user-pass

Step 3 - Setting User Data for the instance

Since we're putting this instance in a auto scaling group, we need to make sure that it will recover when failed, rebooted etc. Everything for this machine is in the UserData.

What this does:

  1. Install necessary packages: aws-cli, curl and openvpn
  2. Copies the "secrets" from S3
  3. Disable the Source/Dest checks for this instance
  4. Configure IP Tables for a routed environment
  5. Adds PAM configuration for LDAP
  6. Installs LDAP client in a non-interactive way
  7. Leaves a nice log in /tmp so you can read it

TODO: Update with an IP Tables that survies a reboot.

 1#!/bin/bash -x
 2function base {
 3  echo "=== Boostrap Starting "
 4  apt-get update
 5  apt-get install -y curl python-pip
 6  DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent
 7  pip install awscli
 8
 9  # IP Configuration
10  aws ec2 modify-instance-attribute --no-source-dest-check \
11      --instance-id `curl http://169.254.169.254/latest/meta-data/instance-id` --region """, Ref("AWS::Region"), """
12
13  cat <<EOT > /etc/iptables/rules.v4
14*nat
15:PREROUTING ACCEPT [85:4110]
16:INPUT ACCEPT [84:4046]
17:OUTPUT ACCEPT [70:11051]
18:POSTROUTING ACCEPT [0:0]
19-A POSTROUTING -s 172.20.0.0/16 -o eth0 -j MASQUERADE
20-A POSTROUTING -s 172.29.0.0/20 -o eth0 -j MASQUERADE
21COMMIT
22# Completed on Tue Mar 28 11:19:00 2017
23# Generated by iptables-save v1.6.0 on Tue Mar 28 11:19:00 2017
24*filter
25:INPUT ACCEPT [2584:919039]
26:FORWARD ACCEPT [0:0]
27:OUTPUT ACCEPT [2388:528346]
28-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
29-A FORWARD -s 172.20.0.0/16 -i eth0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT
30-A FORWARD -s 172.29.0.0/20 -i tun0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT
31-A FORWARD -s 172.29.0.0/20 -d 172.20.0.0/16 -i tun0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT
32COMMIT
33EOT
34
35  iptables-restore < /etc/iptables/rules.v4
36
37  echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
38  sysctl -p
39
40  # OpenVPN configuration
41  mkdir -p /etc/openvpn/keys
42  aws s3 cp s3://my-bucket /etc/openvpn/keys       --recursive --include "ca.crt" --include "server.crt" --include "server.key" --include "dh2048.pem"
43  chmod -R 0600 /etc/openvpn/keys
44  aws s3 cp s3://my-bucket/server.conf /etc/openvpn
45  aws s3 cp s3://my-bucket/ldap.conf /etc/openvpn
46
47  cat <<EOT >/etc/pam.d/openvpn
48auth sufficient pam_ldap.so config=/etc/openvpn/ldap.conf
49auth required pam_deny.so
50account required pam_ldap.so config=/etc/openvpn/ldap.conf
51account required pam_permit.so
52EOT
53
54  DEBIAN_FRONTEND=noninteractive apt-get install -y libpam-ldap
55
56  apt-get install -y openvpn
57  systemctl start openvpn@server
58
59  echo "=== Boostrap complete "
60}
61
62base 2>&1 | tee /tmp/bootstrap.log

Step 4 - Putting it all together

I'm currently using stacker to build my CloudFormation scripts. Here's the generated CF script for your viewing.

I'm using JSON in production, but I've also created a YAML version of the CloudFormation template for your quick review.

OpenVPN AWS Cloudformation

Note: The one thing this setup doesn't do is associate the Public IP of the instance to a DNS name. That's on my future projects list...

Cloudformation OpenVPN template

  1Description: EC2 OpenVPN host
  2Mappings:
  3  AmiMap:
  4    us-east-1:
  5      bastion: ami-2757f631
  6    us-west-2:
  7      bastion: ami-7ac6491a
  8Parameters:
  9  AvailabilityZones:
 10    Description: Availability Zones to deploy instances in.
 11    Type: CommaDelimitedList
 12  DefaultSG:
 13    Description: Top level security group.
 14    Type: AWS::EC2::SecurityGroup::Id
 15  ImageName:
 16    Default: bastion
 17    Description: The image name to use from the AMIMap (usually found in the config
 18      file.)
 19    Type: String
 20  InstanceType:
 21    Default: m3.medium
 22    Description: EC2 Instance Type
 23    Type: String
 24  MaxSize:
 25    Default: "1"
 26    Description: "Maximum # of instances."
 27    Type: Number
 28  MinSize:
 29    Default: "1"
 30    Description: "Minimum # of instances."
 31    Type: Number
 32  OfficeNetwork:
 33    Default: 0.0.0.0/0
 34    Description: CIDR block allowed to connect to bastion hosts.
 35    Type: String
 36  PrivateSubnets:
 37    Description: Subnets to deploy private instances in.
 38    Type: List<AWS::EC2::Subnet::Id>
 39  PublicSubnets:
 40    Description: Subnets to deploy public instances in.
 41    Type: List<AWS::EC2::Subnet::Id>
 42  S3VpnKeysBucketName:
 43    Description: The S3 bucket that contains the keys for the OpenVPN server
 44    Type: String
 45  SshKeyName:
 46    Type: AWS::EC2::KeyPair::KeyName
 47  VpcCidr:
 48    Description: The name of this VPC for tagging
 49    Type: String
 50  VpcId:
 51    Description: Vpc Id
 52    Type: AWS::EC2::VPC::Id
 53  VpcName:
 54    Description: The name of this VPC for tagging
 55    Type: String
 56Resources:
 57  AllowSSHAnywhere:
 58    Type: AWS::EC2::SecurityGroupIngress
 59    Properties:
 60      FromPort: 22
 61      GroupId:
 62        Ref: DefaultSG
 63      IpProtocol: tcp
 64      SourceSecurityGroupId:
 65        Ref: BastionSG
 66      ToPort: 22
 67  BastionAccessPolicy:
 68    Type: AWS::IAM::Policy
 69    Properties:
 70      PolicyDocument:
 71        Statement:
 72          - Action:
 73              - ec2:AssociateAddress
 74              - ec2:Describe
 75              - ec2:ModifyInstanceAttribute
 76            Effect: Allow
 77            Resource: "*"
 78          - Action:
 79              - s3:Get*
 80            Effect: Allow
 81            Resource:
 82              - Fn::Join:
 83                  - ""
 84                  - - "arn:aws:s3:::"
 85                    - Ref: S3VpnKeysBucketName
 86              - Fn::Join:
 87                  - ""
 88                  - - "arn:aws:s3:::"
 89                    - Ref: S3VpnKeysBucketName
 90                    - /*
 91      PolicyName: BastionAccessPolicy
 92      Roles:
 93        - Ref: BastionRole
 94  BastionAutoscalingGroup:
 95    Type: AWS::AutoScaling::AutoScalingGroup
 96    Properties:
 97      AvailabilityZones:
 98        Ref: AvailabilityZones
 99      LaunchConfigurationName:
100        Ref: BastionLaunchConfig
101      MaxSize:
102        Ref: MaxSize
103      MinSize:
104        Ref: MinSize
105      Tags:
106        - Key: Name
107          PropagateAtLaunch: true
108          Value: stage-railz.openvpn
109        - Key: Application
110          PropagateAtLaunch: true
111          Value:
112            Ref: AWS::StackId
113        - Key: network
114          PropagateAtLaunch: true
115          Value: public
116      VPCZoneIdentifier:
117        Ref: PublicSubnets
118  BastionInstanceProfile:
119    Type: AWS::IAM::InstanceProfile
120    Properties:
121      Path: /
122      Roles:
123        - Ref: BastionRole
124  BastionLaunchConfig:
125    Type: AWS::AutoScaling::LaunchConfiguration
126    Properties:
127      AssociatePublicIpAddress: "true"
128      IamInstanceProfile:
129        Ref: BastionInstanceProfile
130      ImageId:
131        Fn::FindInMap:
132          - AmiMap
133          - Ref: AWS::Region
134          - Ref: ImageName
135      InstanceType:
136        Ref: InstanceType
137      KeyName:
138        Ref: SshKeyName
139      SecurityGroups:
140        - Ref: DefaultSG
141        - Ref: BastionSG
142      UserData:
143        Fn::Base64:
144          Fn::Join:
145            - ""
146            - - "#!/bin/bash -x\nfunction base {\n  echo \"=== Boostrap Starting \"\n\
147                \  apt-get update\n  apt-get install -y curl python-pip\n  DEBIAN_FRONTEND=noninteractive\
148                \ apt-get install -y iptables-persistent\n  pip install awscli\n\n \
149                \ # IP Configuration\n  aws ec2 modify-instance-attribute --no-source-dest-check\
150                \       --instance-id `curl http://169.254.169.254/latest/meta-data/instance-id`\
151                \ --region "
152              - Ref: AWS::Region
153              - "\n\n  cat <<EOT > /etc/iptables/rules.v4\n*nat\n:PREROUTING ACCEPT\
154                \ [85:4110]\n:INPUT ACCEPT [84:4046]\n:OUTPUT ACCEPT [70:11051]\n:POSTROUTING\
155                \ ACCEPT [0:0]\n-A POSTROUTING -s 172.20.0.0/16 -o eth0 -j MASQUERADE\n\
156                -A POSTROUTING -s 172.29.0.0/20 -o eth0 -j MASQUERADE\nCOMMIT\n# Completed\
157                \ on Tue Mar 28 11:19:00 2017\n# Generated by iptables-save v1.6.0 on\
158                \ Tue Mar 28 11:19:00 2017\n*filter\n:INPUT ACCEPT [2584:919039]\n:FORWARD\
159                \ ACCEPT [0:0]\n:OUTPUT ACCEPT [2388:528346]\n-A FORWARD -m conntrack\
160                \ --ctstate RELATED,ESTABLISHED -j ACCEPT\n-A FORWARD -s 172.20.0.0/16\
161                \ -i eth0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT\n-A FORWARD -s\
162                \ 172.29.0.0/20 -i tun0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT\n\
163                -A FORWARD -s 172.29.0.0/20 -d 172.20.0.0/16 -i tun0 -o eth0 -m conntrack\
164                \ --ctstate NEW -j ACCEPT\nCOMMIT\nEOT\n\n  iptables-restore < /etc/iptables/rules.v4\n\
165                \n  echo \"net.ipv4.ip_forward = 1\" >> /etc/sysctl.conf\n  sysctl -p\n\
166                \n  # OpenVPN configuration\n\n  mkdir -p /etc/openvpn/keys\n  aws s3\
167                \ cp s3://"
168              - Ref: S3VpnKeysBucketName
169              - " /etc/openvpn/keys       --recursive --include \"ca.crt\" --include\
170                \ \"server.crt\" --include \"server.key\" --include \"dh2048.pem\"\n\
171                \  chmod -R 0600 /etc/openvpn/keys\n  aws s3 cp s3://"
172              - Ref: S3VpnKeysBucketName
173              - "/server.conf /etc/openvpn\n  aws s3 cp s3://"
174              - Ref: S3VpnKeysBucketName
175              - "/ldap.conf /etc/openvpn\n\n  cat <<EOT >/etc/pam.d/openvpn\nauth sufficient\
176                \ pam_ldap.so config=/etc/openvpn/ldap.conf\nauth required pam_deny.so\n\
177                account required pam_ldap.so config=/etc/openvpn/ldap.conf\naccount\
178                \ required pam_permit.so\nEOT\n\n  DEBIAN_FRONTEND=noninteractive apt-get\
179                \ install -y libpam-ldap\n\n  apt-get install -y openvpn \n  systemctl\
180                \ start openvpn@server\n\n  echo \"=== Boostrap complete \"\n}\n\nbase\
181                \ 2>&1 | tee /tmp/bootstrap.log\n"
182  BastionRole:
183    Type: AWS::IAM::Role
184    Properties:
185      AssumeRolePolicyDocument:
186        Statement:
187          - Action: sts:AssumeRole
188            Effect: Allow
189            Principal:
190              Service: ec2.amazonaws.com
191      Path: /
192  BastionSG:
193    Type: AWS::EC2::SecurityGroup
194    Properties:
195      GroupDescription: BastionSecurityGroup
196      SecurityGroupIngress:
197        - CidrIp:
198            Ref: OfficeNetwork
199          FromPort: 1194
200          IpProtocol: udp
201          ToPort: 1194
202      VpcId:
203        Ref: VpcId