OpenVPN Accounts Management

Ich wurde mit dem Wunsch konfrontiert, eine Möglichkeit zu haben, Mitarbeitern, die über einen OpenVPN-Zugang verfügen, diesen temporär aktivieren/deaktivieren zu können. Zudem soll es möglich sein, ein Zeitspanne bis zur Deaktivierung angeben zu können, so dass der Zugang zu einem gewissen Zeitpunkt automatisch deaktiviert wird.

Bisher wurde oft einfach ein Zertifikat mit begrenzter Gültigkeitsdauer erstellt, was aber sehr unkomfortabel wird, soll dies öfters geschehen.

Zuerst habe ich mal geschaut, ob so ein Tool bereits existiert, habe aber leider nichts gefunden und entschieden dann halt selbst eins zu entwickeln.

Das Lesen der OpenVPN Doku gibt dann auch gleich ein paar Anhaltspunkte:

–client-config-dir dir
Specify a directory dir for custom client config files. After a connecting client has been authenticated, OpenVPN will look in this directory for a file having the same name as the client’s X509 common name. If a matching file exists, it will be opened and parsed for client-specific configuration options.
–ccd-exclusive
Require, as a condition of authentication, that a connecting client has a –client-config-dir file.

Beide Parameter in Kombination ermöglichen es einen Zugang zu deaktivieren. Mit –client-config-dir lässt sich ein Verzeichniss festlegen, indem accountspezifische Konfigurationen anglegt werden. Beim Einloggen überprüft dann OpenVPN, ob in diesem Verzeichniss eine Datei liegt, die dem Common Name des Anmeldenden entspricht. Dadurch könnten später auch accountspezifische Firewallregeln angetriggert werden.

Der zweite Parameter sorgt dafür, dass ein Login nur erfolgreich ist, wenn die Datei auch vorhanden ist.

D.h. um einen User zu deaktivieren, muss nur die entsprechende CCD-Datei gelöscht/umbenannt werden, um ihn wieder zu aktivieren, muss eine ( kann leer sein ) CCD-Datei erstellt werden.

Das Ganze soll nun in ein möglichst einfaches Webfrontend gegossen werden. Da ich lieber auf Python als PHP zurückgreife, habe ich mal geschaut, was es dort für einfachste Frameworks gibt und bin auf Bottle gestossen, ein sehr simples Framework, dass aber alles bietet, was ich brauche.

Sicherheit ist bei diesem ersten Test komplett aussen vor, d.h. ich gehe davon aus, dass nur ich selbst Zugang habe.

Hier mal der komplette Quellcode der App, ist ja nicht wirklich viel:

# -*- coding: latin-1 -*-

import os, sys, json, sqlite3, bottle, urllib, socket, re, subprocess as sub
from bottle import request, route, view, redirect, static_file

# rootpath config
ROOTPATH = os.path.dirname(__file__)

# Change working directory so relative paths (and template lookup) work again
os.chdir(os.path.dirname(__file__))

# ... build or import your bottle application here ...
# Do NOT use bottle.run() with mod_wsgi

bottle.debug(True)
app = bottle.Bottle()

### Config ####
BASE_URL = "/"
host="192.168.0.100"
port=7506
version=4
CCD_DIR='/etc/openvpn/ccd-1194/'
DB_DIR =  ROOTPATH + '/state/'
DB_NAME = DB_DIR + 'state.sqlite3'

def do_account_action(action='',account=''):
    if action == "activate":
        cmd = "mv /etc/openvpn/ccd-1194/" + account + "\.deactivated /etc/openvpn/ccd-1194/" + account
    else:
        cmd = "mv /etc/openvpn/ccd-1194/" + account + " /etc/openvpn/ccd-1194/" + account + ".deactivated"
    p = sub.Popen(['/bin/bash', '-c', cmd], stdout=sub.PIPE, stderr=sub.STDOUT)
    output = urllib.unquote(p.stdout.read())

    return output

def get_status():
    sock=connexion(host, port, 'status',version)
    data=sock.interact()
    tab1=re.findall("(.+),(\d+\.\d+\.\d+\.\d+\:\d+),(\d+),(\d+),(.+)", data)
    tab2=re.findall("(\d+\.\d+\.\d+\.\d+),(.+),(\d+\.\d+\.\d+\.\d+\:\d+),(.+)", data)

    num=(len(tab1)+len(tab2))/2

    stat_dict = {}
    for i in xrange(len(tab1)):
       for j in xrange(len(tab2)):
         if tab2[j][1]==tab1[i][0]:
            sendv=float(tab1[i][2])/1024
            receiv=float(tab1[i][3])/1024
            cn = tab1[i][0]            # common name
            ip_extern = tab1[i][1]     # ip intern
            ip_intern = tab2[j][0]     # official client IP
            bytes_send = '%.2f KB' % sendv
            bytes_received = '%.2f KB' % receiv
            tunnel_start = tab1[i][4]
            tunnel_stop =  tab2[j][3]
            stat_dict[cn] = [ ip_intern, ip_extern , bytes_send ,  bytes_received ,  tunnel_start , tunnel_stop ]
    return stat_dict

def get_db_config(account=''):
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    c.execute("SELECT active, enddate FROM state WHERE account=?", [account])
    result = c.fetchall()

    if len(result)==0:
        return { 'active' : False, 'enddate' : '' }
    else:
        return { 'active': result[0][0], 'enddate' : result[0][1] }

def store_db_config(account='', aktiv=False, enddate=''):
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    c.execute("REPLACE into state (account, active, enddate) VALUES (?,?,?)", (account,aktiv,enddate))
    conn.commit()
    c.close()

    return True

@app.route('/')
@app.route('/action/:action/:account')
@view('list-css')
def page(action='', account=''):
    retcode = "OK" + action + "  " + account
    if action == 'activate' or action == 'deactivate':
        retcode = do_account_action(action,account)
        redirect(BASE_URL)
    else:
        ccd_dir="/etc/openvpn/ccd-1194"
        name_list=os.listdir(ccd_dir)
        name_list.sort()
        stat_dict = get_status()

        # check if timerestriction is active
        timeresconfig = {}
        for name in name_list:
                timeresconfig[name] = get_db_config(name)
        return dict(names=name_list, status=stat_dict, trc = timeresconfig)

@app.route('/timeform', method='POST')
@view('printform')
def form():
    account = request.forms.get('account')
    active =  request.forms.get('active')
    enddate =  request.forms.get('enddate')
    if active:
        aktiv = True
    else:
        aktiv = False
    # now set the time restriction and redirect to index page
    status = store_db_config(account,aktiv,enddate)

    if status:
        redirect(BASE_URL)
    formdata = {}
    # Error, print Error
    formdata['keys'] = request.forms.keys()
    formdata['values'] = request.forms.values()
    return dict(form_data = formdata)

application = app