OpenVPN Server Configuration Script – Ubiquiti EdgeRouter Lite

=The lazy way to configure OpenVPN Server on a Ubiquiti EdgeRouter Lite=

I have a Ubiquiti EdgeRouter Lite that I use as a staging platform for systems in production. Because I have had to reconfigure the VPN so many times on this device, I created a simple Python tool to run through the entire process for me. Instructions for installing/using the script are detailed below; if you would like to read a tutorial on how to do everything manually, check out [[configure-openvpn-with-x-509-ubiquiti-edgerouter-lite]]

** As noted by @dazo in the comments, this setup is meant for a lab environment. Hosting your CA on the VPN server is considered bad practice, and the keys generated by the EdgeRouter will be cryptographically sub-par to that of a standard machine **

== Installation ==
# [[https://blog.laslabs.com/2013/06/initial-configuration-ubiquiti-edgerouter-lite/#repos|Add Debian Repos]]
# Install Python’s package manager
{{{ lang=bash
sudo apt-get install python-setuptools
}}}
# Now we install [[http://www.noah.org/python/pexpect/|Pexpect]]
{{{ lang=bash
sudo easy_install pexpect
}}}
# Download [[#the-script|this script]] into any directory on the router. Mine is located at `/config/auth/erl_vpn_configure.py`.
# Open the script in your favorite text editor. Edit between the `# BEGIN EDITING` and `# END EDITING` blocks in order to suit your needs.

== Usage ==
* To run a complete OpenVPN server setup, including CA generation, simply run the script with `python /path/to/script`
// OR //
# Enter the python interpreter by typing `python` into your shell prompt.
# Create a new object to manipulate your server
{{{ lang=python
ca = ca_obj()
}}}
Use the below table as a function reference. Use a function with the following syntax `ca.function(parameter1, parameter2, ..)`
{|
|-
! Function Name
! Description
! Parameters
|-
| complete_setup
| Perform complete OpenVPN server setup
|
|-
| setup_erl_server
| Perform Vyatta/ERL OpenVPN configuration
|
|-
| setup_client_certs
| Generate client certificates, uses clients defined as CLIENTS variable in config
|
|-
| gen_ca
| Generate CA Cert
| {{{ str filename Filename of new cacert }}}
|-
| new_req
| New cert signing request
| {{{ str pem_pass PEM Pass for cert
dic req_opts Request options }}}
|-
| sign_req
| Sign cert request
| {{{ str cn CN of the certificate, will be the new filenames
bool strip_pass Strip PEM pass?
str passwd PEM Password }}}
|-
| gen_dhp
| Generate Diffie-Helman Parameters
|
|-
| strip_pem_pass
| Strip PEM pass from key file
| {{{ str file_in
str passwd PEM passphrase
str file_out }}}
|}
== Samples ==
# Completely setup CA Cert and OpenVPN server
{{{ lang=python
ca = ca_obj
ca.complete_setup()
}}}
//More Coming Soon//
== The Script ==
* [[https://repo.laslabs.com/projects/RND/repos/toolbox/browse/erl_vpn_configure.py|LasLabs Repo]]

{{{ lang=python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##
# VPN Configure-O-Matic
#
# Quickly sets up a VPN server, complete with client/cert config.
# Specifically created for EdgeRouter Lite;
# Probably works on any Linux distro..
# Client configuration can be performed via ssh
#
# @author David Lasley
# @package toolbox

import pexpect
import subprocess
import os
from threading import Thread
from itertools import chain

”’
BEGIN EDITING
”’

CA_PASS = ‘CA_PASS’ #< CA Cert pass HOST_PASS = 'HOST_PASS' #< Host key pass CLIENT_PASS = 'CLIENT_PASS' #< Client key pass ''' Where the CA scripts are located on the server.. probably won't need to change this ''' SSL_DIR = '/usr/lib/ssl/misc/' ''' Where to save all of the certs/keys on the server.. probably won't need to change this ''' SAVE_DIR = '/config/auth/' ''' Default cert configuration options. These can be overridden in the individual configs. ''' DEFAULT_CERT = { 'country':'US', 'state':'Nevada', 'city':'Las Vegas', 'company':'LasLabs', 'ou':'', 'email':'dave@domain.com', 'pass':'', 'opt_company':'', } # CA cert configuration overrides, always set 'cn' CA_CERT = { 'cn':'router.domain.com', } # Host cert configuration overrides, always set 'cn' HOST_CERT = { 'cn':'vpn.domain.com', } ''' Client SSH users. {} to skip client configuration. Set key-file to file path of private key file if applicable ''' CLIENT_CREDS = {'uname':'username', 'passwd':'password', 'key-file':None} # Host cert configuration overrides, always set 'cn' CLIENTS = [ {'cn':'atl-vps-01.domain.com', 'city':'Atlanta', 'state':'Georgia', 'remote-creds':CLIENT_CREDS}, {'cn':'los-vps-01.domain.com', 'city':'Los Angeles', 'state':'California', 'remote-creds':CLIENT_CREDS}, {'cn':'chi-vps-01.domain.com', 'city':'Chicago', 'state':'Illinois', 'remote-creds':CLIENT_CREDS}, {'cn':'dlasley.vpn.domain.com', 'remote-creds':CLIENT_CREDS} ] # Diffie-helman filename, probably won't need to change DHP_PATH = os.path.join(SAVE_DIR, 'dhp.pem') DHP_BITS = 1024 #< Diffie-Helman Param Bits, probably won't need to change TUNNEL_SETTINGS = { #< VPN Tunnel settings 'vtun0':{ 'mode':'server', 'server subnet':'192.168.68.0/24', #'local-port':'1195', # Routes to LAN/WLAN, pushed to client 'server push-route':['192.168.69.0/24',], # Clients with static IPs 'server client':['atl-vps-01.domain.com ip 192.168.68.100', 'chi-vps-01.domain.com ip 192.168.68.101', 'los-vps-01.domain.com ip 192.168.68.102', ], # TLS Keys/Certs, probably won't need to change 'tls ca-cert-file':os.path.join(SAVE_DIR, 'demoCA', 'cacert.pem'), 'tls cert-file':'%s.pem' % os.path.join(SAVE_DIR, HOST_CERT['cn']), 'tls key-file':'%s.key' % os.path.join(SAVE_DIR, HOST_CERT['cn']), 'tls dh-file':DHP_PATH, } } # # END EDITING # CERT_MAP = [ ['Country Name .*:','country'], ['State or Province Name .*:','state'], ['Locality Name .*:','city'], ['Organization Name .*:','company'], ['Organizational Unit Name .*:','ou'], ['Common Name .*:','cn'], ['Email Address .*:','email'], ['A challenge password .*:','pass'], ['An optional company name .*','opt_company'], ] class erl_obj(object): VYATTA_SHELL_API = '/bin/cli-shell-api' VYATTA_SBIN = '/opt/vyatta/sbin/' def __init__(self, strip_host_pass=True, strip_client_passes=True, ): ''' Init ''' self.ca_sh = os.path.join(SSL_DIR, 'CA.sh') self.strip_host_pass = strip_host_pass self.strip_client_passes = strip_client_passes self.log_file = open('/var/log/erl_vpn.log', 'w') if not os.path.isdir(SAVE_DIR): os.mkdir(SAVE_DIR) os.chdir(SAVE_DIR) def complete_setup(self, ): ''' Perform complete OpenVPN server setup ''' democa = os.path.join(SAVE_DIR, 'demoCA') if os.path.isdir(democa): os.rmdir(democa) # Diffie-Helman Thread dhp_thread = Thread(target=self.gen_dhp) dhp_thread.daemon = False dhp_thread.start() # Create CA self.gen_ca() # Host cert/key req_opts = dict(chain(DEFAULT_CERT.items(), HOST_CERT.items())) self.new_req(HOST_PASS, req_opts) self.sign_req(HOST_CERT['cn'], self.strip_host_pass, HOST_PASS) # Client certs/keys self.setup_client_certs() # Wait for Diffie-Helman dhp_thread.join() # Configure the server self.setup_erl_server() def setup_erl_server(self, ): ''' Perform Vyatta/ERL server configuration ''' SET = os.path.join(self.VYATTA_SBIN, 'my_set') COMMIT = os.path.join(self.VYATTA_SBIN, 'my_commit') SAVE = os.path.join(self.VYATTA_SBIN, 'vyatta-save-config.pl') commands = [ 'session_env=$(%s getSessionEnv $PPID)' % self.VYATTA_SHELL_API, 'eval $session_env', '%s setupSession' % self.VYATTA_SHELL_API, ] for tun_name, tun_settings in TUNNEL_SETTINGS.iteritems(): tun_node = 'interfaces openvpn %s' % tun_name for setting_name, setting in tun_settings.iteritems(): if type(setting) == list: #< Allow iteration for _setting in setting: commands.append('%s %s %s %s' % ( SET, tun_node, setting_name, _setting)) else: commands.append('%s %s %s %s' % ( SET, tun_node, setting_name, setting)) commands.append(COMMIT) commands.append(SAVE) self.log_file.write( ' && '.join(commands) + '\r\n' ) subprocess.call([' && '.join(commands)], shell=True) return def setup_erl_client(self, config): ''' Perform Vyatta/ERL remote client config @param dict config Client configuration ''' try: if config['remote-creds']['uname']: if config['remote-creds'].get('key-file'): #< Key auth cmd = 'ssh -i "%s" %s@%s' % ( config['remote-creds']['key-file'], config['remote-creds']['uname'], config.get('ip') or config['cn'] ) elif config['remote-creds'].get('passwd'): #< Password cmd = 'ssh -i %s:%s@%s' % ( config['remote-creds']['uname'], config['remote-creds']['passwd'], config.get('ip') or config['cn'] ) console = pexpect.spawn except KeyError: #< No remote creds return False def setup_client_certs(self, ): ''' Generate client certificates ''' for client in CLIENTS: req_opts = dict(chain(DEFAULT_CERT.items(), client.items())) self.new_req(CLIENT_PASS, req_opts) self.sign_req(client['cn'], self.strip_client_passes, CLIENT_PASS) return def gen_ca(self, filename='', ): ''' Generate CA Cert @param str filename Filename of new cacert ''' ca_opts = dict(chain(DEFAULT_CERT.items(), CA_CERT.items())) console = pexpect.spawn('%s -newca' % self.ca_sh, logfile=self.log_file) console.expect([pexpect.EOF, 'CA certificate filename .*']) console.sendline(filename) self._generic_request(console, CA_PASS, ca_opts) console.expect([pexpect.EOF, 'Enter pass phrase for .*:']) console.sendline(CA_PASS) console.expect([pexpect.EOF, 'Data Base Updated']) return def new_req(self, pem_pass, req_opts): ''' New cert signing request @param str pem_pass PEM Pass for cert @param dic req_opts Request options ''' console = pexpect.spawn('%s -newreq' % self.ca_sh, logfile=self.log_file) self._generic_request(console, pem_pass, req_opts, True) return def sign_req(self, cn, strip_pass=True, passwd='12345'): ''' Sign cert request @param str cn CN of the certificate, will be the new filenames @param bool strip_pass Strip PEM pass? @param str passwd PEM Password ''' console = pexpect.spawn('%s -sign' % self.ca_sh, logfile=self.log_file) console.expect([pexpect.EOF, 'Enter pass phrase for .*:']) console.sendline(CA_PASS) console.expect([pexpect.EOF, 'Sign the certificate\? .*:']) console.sendline('y') console.expect([pexpect.EOF, '1 out of 1 certificate requests certified, .*']) console.sendline('y') console.expect([pexpect.EOF, 'Signed certificate is .*']) os.rename(os.path.join(SAVE_DIR, 'newcert.pem'), os.path.join(SAVE_DIR, '%s.pem' % cn)) key_file = os.path.join(SAVE_DIR, '%s.key' % cn) os.rename(os.path.join(SAVE_DIR, 'newkey.pem'), key_file) if strip_pass: self.strip_pem_pass(key_file, passwd) return def gen_dhp(self, ): ''' Generate Diffie-Helman Parameters... This should be threaded ''' subprocess.check_call(['openssl', 'dhparam', '-out', DHP_PATH, '-2', str(DHP_BITS)]) return def strip_pem_pass(self, file_in, passwd, file_out=None): ''' Strip PEM pass from key file @param str file_in @param str passwd PEM passphrase @param str file_out ''' if not file_out: file_out = file_in console = pexpect.spawn('openssl rsa -in %s -out %s' % (file_in, file_out), logfile=self.log_file) console.expect([pexpect.EOF, 'Enter pass phrase for .*:']) console.sendline(passwd) console.expect([pexpect.EOF, 'writing RSA key']) return def _generic_request(self, console, pem_pass, req_opts, wait_for_it=False): ''' Generic cert request @param pexpect console Pexpect console to manipulate @param str pem_pass PEM Pass for cert @param dic req_opts Request options ''' console.expect([pexpect.EOF, 'Enter PEM pass phrase:']) console.sendline(pem_pass) console.expect([pexpect.EOF, 'Verifying - Enter PEM pass phrase:']) console.sendline(pem_pass) for map_part in CERT_MAP: console.expect([pexpect.EOF, map_part[0]]) console.sendline(req_opts[map_part[1]]) if wait_for_it: console.expect([pexpect.EOF, 'Request is in .* newkey\.pem']) return if __name__ == '__main__': erl_obj = erl_obj() erl_obj.complete_setup() }}}


Posted

in

,

by

Comments

16 responses to “OpenVPN Server Configuration Script – Ubiquiti EdgeRouter Lite”

  1. Jeff Avatar
    Jeff

    Kudos on a great script! It ran perfectly, however, I’m unsure what the client configuration should look like. Any chance you could provide an example? I’d be interested in extending the script to generate a client configuration. :-)

    1. dlasley Avatar

      I’m happy to hear that the script worked for you. Client setup is relatively simple on these units, I went ahead and posted the steps here. Adding client configuration to the script may not be a bad idea.. to take it a step further, it would be possible to use pexpect or paramiko to allow remote setup via ssh. I’ll update this post once I finish; thanks for reading my blog :)

  2. Hans Avatar

    Hi Dave,

    Thanks for the script. I tried to make the appropriate changes and ran the script however ran into the following error when running the script.

    root@www:/config/auth# python ./erl_vpn_configure.py
    Traceback (most recent call last):
    File “./erl_vpn_configure.py”, line 300, in
    erl_obj.complete_setup()
    File “./erl_vpn_configure.py”, line 132, in complete_setup
    democa = os.path.join(self.SAVE_DIR, ‘demoCA’)
    AttributeError: ‘erl_obj’ object has no attribute ‘SAVE_DIR’

    I tried googling for the error, however can’t seem to find out what i did wrong.
    I verified the lines 300 and 132 with the your script and they are identical. never made changes to this area of the script either.

    Any idea what i’m doing wrong?

    1. Dave Lasley Avatar

      That was actually an error on my part, sorry about that.

      I have updated the script above – I removed the `self.` from `self.SAVE_DIR` on line 132 if you would like to make the change yourself instead of a patch.

  3. dazo Avatar
    dazo

    This article actually fails the “How CAs work 101” training.

    First of all, you should never generate keying material on embedded devices unless you have a firm confirmation that the random number generator is fed with enough entropy. With a weak RNG, you get weak keys.

    Secondly, you should as far as possible avoid having the CA key on a device which is online, especially directly on VPN servers. If you loose the CA key, all the certificates must be considered broken. Those being able to sign new certificates with your CA key controls your CA completely – even if you don’t know about it. Ideally the CA database and in particular the CA key should be stored in an offline media which is only physically enabled when needed. And you only need that key when you want to generate new certificates or CRL files.

    That means: the CA key should never ever reside on the same box as the OpenVPN server. The server needs 4 files to function (ca.crt, server.key, server.crt and dh*pem). Clients only need three files (ca.crt, client.key, client.crt).

    Even though /you/ might be clever enough to only do this on test boxes for a test environment and ensure this does not hit real production environment, that’s good. But there are just too many users who does not understand this and learns these stupid mistakes and believes it is correct to do so.

  4. Lucas Jans Avatar
    Lucas Jans

    I’m stuck on step 1! Running 1.7.

    >lucasjans@ubnt:~$ sudo apt-get install python-setuptools
    Reading package lists… Done
    Building dependency tree… Done
    Some packages could not be installed. This may mean that you have
    requested an impossible situation or if you are using the unstable
    distribution that some required packages have not yet been created
    or been moved out of Incoming.
    The following information may help to resolve the situation:
    The following packages have unmet dependencies:
    python-setuptools : Depends: python (< 2.7) but 2.7.3-4+deb7u1 is to be installed
    Depends: python-pkg-resources (= 0.6.14-4) but it is not going to be installed
    E: Unable to correct problems, you have held broken packages.
    The following packages have unmet dependencies:
    python-setuptools : Depends: python (< 2.7) but 2.7.3-4+deb7u1 is to be installed
    Depends: python-pkg-resources (= 0.6.14-4) but it is not going to be installed
    E: Unable to correct problems, you have held broken packages.

    1. Dave Lasley Avatar

      Hi Lucas,

      That error seems like the proper repos are not on your system. Did you follow the instructions to install the Debian repos in Step 1?

      Once the repos are installed, try:

      sudo apt-get update
      sudo apt-get install -f
      sudo apt-get install python-setuptools

      1. Chris Avatar
        Chris

        I had the same error – and tried what you suggested but still get the same error.

      2. Lucas Jans Avatar
        Lucas Jans

        lucasjans@ubnt# show system package
        repository squeeze {
        components “main contrib non-free”
        distribution squeeze
        url http://http.us.debian.org/debian
        }
        repository squeeze-security {
        components main
        distribution squeeze/updates
        url http://security.debian.org
        }
        [edit]

        1. Dave Lasley Avatar

          Interesting, your repos seem fine. I’m on 1.6 but I don’t see why this would make a difference. I’ll upgrade firmware sometime this week and get back to you.

        2. Mark Avatar
          Mark

          You need to remove those repositories and re-add them, but replacing “squeeze” with “wheezy”. Firmware 1.7 is based on Debian 7.8

          1. Dave Lasley Avatar

            Good call, thanks Mark. Info verified and post updated.

  5. Chandra Avatar
    Chandra

    Hi
    I want to setup Opnevpn Server on UBNT EdgeRouter Lite-3 with Firmware version 1.7. I need client authentication as username and password. No certificates please.

    Please help. I have followed many “How to Dos”, but none of them are working with 1.7 version.

    ./build-ca is not working at all. error throwing like”openssl.cnf file is not correct version………..”

    tried many things.

    Please hope

  6. Aaron Luders Avatar
    Aaron Luders

    Not sure what I’m doing wrong. I get this when I run it…

    python openvpn-setup.py
    Generating DH parameters, 1024 bit long safe prime, generator 2
    This is going to take a long time
    ……………….+………………………………………………………………………Traceback (most recent call last):
    File “openvpn-setup.py”, line 292, in
    erl_obj.complete_setup()
    File “openvpn-setup.py”, line 136, in complete_setup
    self.sign_req(HOST_CERT[‘cn’], self.strip_host_pass, HOST_PASS)
    File “openvpn-setup.py”, line 245, in sign_req
    os.path.join(SAVE_DIR, ‘%s.pem’ % cn))
    OSError: [Errno 2] No such file or directory
    ………………………….+…………………………………+……………………….+………………………………………………………….+……………………………+…..

    1. Dave Lasley Avatar

      Looks like the script is not able to find the output directory. Is the `SAVE_DIR` variable still declared as `/config/auth/`?

      If so, check to see that the directory exists – `ls /config/auth`

      If it doesn’t, make it – `mkdir /config/auth`

      Then try the script again.

  7. Rob Robinett Avatar
    Rob Robinett

    Thanks for posting this script which is just what I need,
    However I am not familiar with Python and on 1.9.7 I can’t understand what I need to do in step 1 to install the needed libraries.
    Python 2.7.3 is installed as part of the OS, but it isn’t clear to me what to do next.
    thanks

Leave a Reply

Your email address will not be published. Required fields are marked *