Archiv für den Monat: Mai 2012

Proxmox 2.1 Remote mit TigerVNC

Zur Zeit migriere ich eine VmWARE ESXi 4 Infrastruktur mit 2 Servern Richtung KVM. Als Management-Tool habe ich mit Proxmox rausgeschaut, dass auf einem Testserver bereits seit Version 1.9 stabil läuft.

Das Zugreifen auf eine VM über Remote Tools ist  ein „Must Have“. Folgendes Szenario erfordert es:

In einer VM wird eine VPN Verbindung konfiguriert, die den Defaultgateway in das verbundene Netzwerk verlegt. Sobald die VPN-Verbindung also offen ist, kann auf die Maschine nicht mehr mit RDP zugegriffen werden, es geht nur noch über die vom VM-Host angebotenen Remote-Tools.

Proxmox benutzt hierfür ein Java-VNC-Applet, was auch gut funktioniert. Nur ist es jetzt so, dass das Javaplugin mittlerweile von vielen als Sicherheitsrisiko angesehen wird und somit nicht zur Verfügung steht. Also musste eine Lösung gefunden werden, direkt mit einem VNC-Viewer eine Verbindung aufbauen zu können.

Als erstes galt es herrauszubekommen wie das mit dem Java-Applet funktioniert. Wenn man die Console startet, passieren 2 Dinge:

  1. Es wird nc -l -p 5900 -c „qm vncproxy VMID“ auf dem Host ausgeführt
  2. Es wird der Java Browser mit der entsprechenden Konfiguration gestartet

Jetzt wird das Ganze schon ein bissen klarer. Wenn man nun einfach einen Port öffnet und bei Connection ebenfalls den Qemu Vncproxy antriggert sollte es doch gehen.

Also gleich mal xinetd installiert ( geht natürlich auch mit openbsd-inetd ) und für einen Test folgende Konfiguration angelegt:

service vnc-10100
{
socket_type             = stream
wait                    = no
user                    = root
server                  = /usr/sbin/qm
server_args             = vncproxy 100
#log_on_failure += USERID
}

Jetzt noch in /etc/services den neuen Service eintragen:

vnc-10100       10100/tcp                        # vncproxy for Testmaschine

Nach einem Restart von xinetd kann man sich nun mit VNC mit den einzelnen Maschinen verbinden:

vncviewer.exe <IP Proxmox Host>::<Port der VM>

Meinen ersten Versuch mit TightVNC war schon vielversprechend, nur gab es Protokollprobleme, was an den benutzten X.509 Zertifikaten liegt, ein Feature, das weder Ultra- noch TightVNC bieten. Zum Glück gibt es mit TigerVNC einen Fork von TightVNC 4.0, der um dieses Feature erweitert wurde.

Die aktuelle Version 1.2 hat leider ein Problem mit dem Menü zum Senden von Sonderzeichen ( VNC bekommt ein Verbindungsproblem sobald man F8 antriggert ), aber Version 1.10 funktioniert wunderbar.

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