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 ∞
- Add Debian Repos
- Install Python’s package manager
sudo apt-get install python-setuptools
- Now we install Pexpect
sudo easy_install pexpect
- Download 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
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 | dic req_opts Request options |
sign_req | Sign cert request |
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 passwd PEM passphrase str file_out |
Samples ∞
- Completely setup CA Cert and OpenVPN server
ca = ca_obj ca.complete_setup()
More Coming Soon
The Script ∞
#!/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 <dave@dlasley.net> # @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()0
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. :-)
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 :)
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?
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.
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.
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.
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
I had the same error – and tried what you suggested but still get the same error.
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]
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.
You need to remove those repositories and re-add them, but replacing “squeeze” with “wheezy”. Firmware 1.7 is based on Debian 7.8
Good call, thanks Mark. Info verified and post updated.
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
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
………………………….+…………………………………+……………………….+………………………………………………………….+……………………………+…..
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.
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