OpenVPN on AWS VPC with LDAP

Getting OpenVPN working on your AWS VPC

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

port 1194
proto udp
dev tun
server 172.29.0.0 255.255.240.0
push "route 172.20.0.0 255.255.0.0"
ca /etc/openvpn/keys/ca.crt
cert /etc/openvpn/keys/server.crt
key /etc/openvpn/keys/server.key
dh /etc/openvpn/keys/dh2048.pem
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
comp-lzo
persist-key
persist-tun
status openvpn-status.log
log-append  /var/log/openvpn.log
verb 3
max-clients 100
user nobody
group nogroup
plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so openvpn

ldap.conf - using jumpcloud

host ldap.jumpcloud.com
base ou=Users,o=SECRET,dc=jumpcloud,dc=com
binddn uid=ldap-aws,ou=Users,o=SECRET,dc=jumpcloud,dc=com
bindpw VERYSECRET
scope one
timelimit 5
bind_timelimit 2
bind_policy soft
idle_timelimit 6

pam_login_attribute uid

pam_min_uid 1000
pam_password exop

nss_base_passwd ou=Users,o=SECRET,dc=jumpcloud,dc=com
nss_base_shadow ou=Users,o=SECRET,dc=jumpcloud,dc=com

ssl start_tls
tls_checkpeer no

Client Configuration – Tunnelblick files

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

client.conf

client
dev tun
proto udp
remote YOUR.DNS.NAME 1194
ca ca.crt
cert yourorg.crt
key yourorg.key
tls-version-min 1.2
tls-cipher TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256
cipher AES-256-CBC
auth SHA512
resolv-retry infinite
auth-retry none
nobind
persist-key
persist-tun
ns-cert-type server
comp-lzo
verb 3
auth-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.

#!/bin/bash -x
function base {
  echo "=== Boostrap Starting "
  apt-get update
  apt-get install -y curl python-pip
  DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent
  pip install awscli

  # IP Configuration
  aws ec2 modify-instance-attribute --no-source-dest-check \
      --instance-id `curl http://169.254.169.254/latest/meta-data/instance-id` --region """, Ref("AWS::Region"), """

  cat <<EOT > /etc/iptables/rules.v4
*nat
:PREROUTING ACCEPT [85:4110]
:INPUT ACCEPT [84:4046]
:OUTPUT ACCEPT [70:11051]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 172.20.0.0/16 -o eth0 -j MASQUERADE
-A POSTROUTING -s 172.29.0.0/20 -o eth0 -j MASQUERADE
COMMIT
# Completed on Tue Mar 28 11:19:00 2017
# Generated by iptables-save v1.6.0 on Tue Mar 28 11:19:00 2017
*filter
:INPUT ACCEPT [2584:919039]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [2388:528346]
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -s 172.20.0.0/16 -i eth0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT
-A FORWARD -s 172.29.0.0/20 -i tun0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT
-A FORWARD -s 172.29.0.0/20 -d 172.20.0.0/16 -i tun0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT
COMMIT
EOT

  iptables-restore < /etc/iptables/rules.v4

  echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
  sysctl -p

  # OpenVPN configuration
  mkdir -p /etc/openvpn/keys
  aws s3 cp s3://my-bucket /etc/openvpn/keys       --recursive --include "ca.crt" --include "server.crt" --include "server.key" --include "dh2048.pem"
  chmod -R 0600 /etc/openvpn/keys
  aws s3 cp s3://my-bucket/server.conf /etc/openvpn
  aws s3 cp s3://my-bucket/ldap.conf /etc/openvpn

  cat <<EOT >/etc/pam.d/openvpn
auth sufficient pam_ldap.so config=/etc/openvpn/ldap.conf
auth required pam_deny.so
account required pam_ldap.so config=/etc/openvpn/ldap.conf
account required pam_permit.so
EOT

  DEBIAN_FRONTEND=noninteractive apt-get install -y libpam-ldap

  apt-get install -y openvpn
  systemctl start openvpn@server

  echo "=== Boostrap complete "
}

base 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

Description: EC2 OpenVPN host
Mappings:
  AmiMap:
    us-east-1:
      bastion: ami-2757f631
    us-west-2:
      bastion: ami-7ac6491a
Parameters:
  AvailabilityZones:
    Description: Availability Zones to deploy instances in.
    Type: CommaDelimitedList
  DefaultSG:
    Description: Top level security group.
    Type: AWS::EC2::SecurityGroup::Id
  ImageName:
    Default: bastion
    Description: The image name to use from the AMIMap (usually found in the config
      file.)
    Type: String
  InstanceType:
    Default: m3.medium
    Description: EC2 Instance Type
    Type: String
  MaxSize:
    Default: '1'
    Description: 'Maximum # of instances.'
    Type: Number
  MinSize:
    Default: '1'
    Description: 'Minimum # of instances.'
    Type: Number
  OfficeNetwork:
    Default: 0.0.0.0/0
    Description: CIDR block allowed to connect to bastion hosts.
    Type: String
  PrivateSubnets:
    Description: Subnets to deploy private instances in.
    Type: List<AWS::EC2::Subnet::Id>
  PublicSubnets:
    Description: Subnets to deploy public instances in.
    Type: List<AWS::EC2::Subnet::Id>
  S3VpnKeysBucketName:
    Description: The S3 bucket that contains the keys for the OpenVPN server
    Type: String
  SshKeyName:
    Type: AWS::EC2::KeyPair::KeyName
  VpcCidr:
    Description: The name of this VPC for tagging
    Type: String
  VpcId:
    Description: Vpc Id
    Type: AWS::EC2::VPC::Id
  VpcName:
    Description: The name of this VPC for tagging
    Type: String
Resources:
  AllowSSHAnywhere:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      FromPort: 22
      GroupId:
        Ref: DefaultSG
      IpProtocol: tcp
      SourceSecurityGroupId:
        Ref: BastionSG
      ToPort: 22
  BastionAccessPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
        - Action:
          - ec2:AssociateAddress
          - ec2:Describe
          - ec2:ModifyInstanceAttribute
          Effect: Allow
          Resource: '*'
        - Action:
          - s3:Get*
          Effect: Allow
          Resource:
          - Fn::Join:
            - ''
            - - 'arn:aws:s3:::'
              - Ref: S3VpnKeysBucketName
          - Fn::Join:
            - ''
            - - 'arn:aws:s3:::'
              - Ref: S3VpnKeysBucketName
              - /*
      PolicyName: BastionAccessPolicy
      Roles:
      - Ref: BastionRole
  BastionAutoscalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AvailabilityZones:
        Ref: AvailabilityZones
      LaunchConfigurationName:
        Ref: BastionLaunchConfig
      MaxSize:
        Ref: MaxSize
      MinSize:
        Ref: MinSize
      Tags:
      - Key: Name
        PropagateAtLaunch: true
        Value: stage-railz.openvpn
      - Key: Application
        PropagateAtLaunch: true
        Value:
          Ref: AWS::StackId
      - Key: network
        PropagateAtLaunch: true
        Value: public
      VPCZoneIdentifier:
        Ref: PublicSubnets
  BastionInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
      - Ref: BastionRole
  BastionLaunchConfig:
    Type: AWS::AutoScaling::LaunchConfiguration
    Properties:
      AssociatePublicIpAddress: 'true'
      IamInstanceProfile:
        Ref: BastionInstanceProfile
      ImageId:
        Fn::FindInMap:
        - AmiMap
        - Ref: AWS::Region
        - Ref: ImageName
      InstanceType:
        Ref: InstanceType
      KeyName:
        Ref: SshKeyName
      SecurityGroups:
      - Ref: DefaultSG
      - Ref: BastionSG
      UserData:
        Fn::Base64:
          Fn::Join:
          - ''
          - - "#!/bin/bash -x\nfunction base {\n  echo \"=== Boostrap Starting \"\n\
              \  apt-get update\n  apt-get install -y curl python-pip\n  DEBIAN_FRONTEND=noninteractive\
              \ apt-get install -y iptables-persistent\n  pip install awscli\n\n \
              \ # IP Configuration\n  aws ec2 modify-instance-attribute --no-source-dest-check\
              \       --instance-id `curl http://169.254.169.254/latest/meta-data/instance-id`\
              \ --region "
            - Ref: AWS::Region
            - "\n\n  cat <<EOT > /etc/iptables/rules.v4\n*nat\n:PREROUTING ACCEPT\
              \ [85:4110]\n:INPUT ACCEPT [84:4046]\n:OUTPUT ACCEPT [70:11051]\n:POSTROUTING\
              \ ACCEPT [0:0]\n-A POSTROUTING -s 172.20.0.0/16 -o eth0 -j MASQUERADE\n\
              -A POSTROUTING -s 172.29.0.0/20 -o eth0 -j MASQUERADE\nCOMMIT\n# Completed\
              \ on Tue Mar 28 11:19:00 2017\n# Generated by iptables-save v1.6.0 on\
              \ Tue Mar 28 11:19:00 2017\n*filter\n:INPUT ACCEPT [2584:919039]\n:FORWARD\
              \ ACCEPT [0:0]\n:OUTPUT ACCEPT [2388:528346]\n-A FORWARD -m conntrack\
              \ --ctstate RELATED,ESTABLISHED -j ACCEPT\n-A FORWARD -s 172.20.0.0/16\
              \ -i eth0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT\n-A FORWARD -s\
              \ 172.29.0.0/20 -i tun0 -o eth0 -m conntrack --ctstate NEW -j ACCEPT\n\
              -A FORWARD -s 172.29.0.0/20 -d 172.20.0.0/16 -i tun0 -o eth0 -m conntrack\
              \ --ctstate NEW -j ACCEPT\nCOMMIT\nEOT\n\n  iptables-restore < /etc/iptables/rules.v4\n\
              \n  echo \"net.ipv4.ip_forward = 1\" >> /etc/sysctl.conf\n  sysctl -p\n\
              \n  # OpenVPN configuration\n\n  mkdir -p /etc/openvpn/keys\n  aws s3\
              \ cp s3://"
            - Ref: S3VpnKeysBucketName
            - " /etc/openvpn/keys       --recursive --include \"ca.crt\" --include\
              \ \"server.crt\" --include \"server.key\" --include \"dh2048.pem\"\n\
              \  chmod -R 0600 /etc/openvpn/keys\n  aws s3 cp s3://"
            - Ref: S3VpnKeysBucketName
            - "/server.conf /etc/openvpn\n  aws s3 cp s3://"
            - Ref: S3VpnKeysBucketName
            - "/ldap.conf /etc/openvpn\n\n  cat <<EOT >/etc/pam.d/openvpn\nauth sufficient\
              \ pam_ldap.so config=/etc/openvpn/ldap.conf\nauth required pam_deny.so\n\
              account required pam_ldap.so config=/etc/openvpn/ldap.conf\naccount\
              \ required pam_permit.so\nEOT\n\n  DEBIAN_FRONTEND=noninteractive apt-get\
              \ install -y libpam-ldap\n\n  apt-get install -y openvpn \n  systemctl\
              \ start openvpn@server\n\n  echo \"=== Boostrap complete \"\n}\n\nbase\
              \ 2>&1 | tee /tmp/bootstrap.log\n"
  BastionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            Service: ec2.amazonaws.com
      Path: /
  BastionSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: BastionSecurityGroup
      SecurityGroupIngress:
      - CidrIp:
          Ref: OfficeNetwork
        FromPort: 1194
        IpProtocol: udp
        ToPort: 1194
      VpcId:
        Ref: VpcId
Share Comments
comments powered by Disqus