=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() }}}
Leave a Reply to Hans Cancel reply