Open-Source-Software für das PKI-Management von Tausenden VPN-Nutzern? Kaum praxistauglich, meinen viele. Stimmt nicht, wir nutzen erfolgreich das mit Open VPN mitgelieferte Easy RSA, erklären die Admins der Charité in Berlin. Der Artikel liefert passende Skripte für den virtuellen Schlüsselschrank.
In den Open-VPN-Seminaren, die der Autor des vorliegenden Artikels gelegentlich gibt, bringen die Teilnehmer gerne Wünsche und Ideen auf den Tisch. Beim letzten Seminar kam eine von einem Windkraft-Erzeuger, der Open VPN [1] auf smarten Windrädern einsetzt. Insgesamt zeigte er sich sehr zufrieden mit der Software, weil sie beispielsweise eine Bandbreiten-Limitierung, eine einfache High Availability und flexibles Trafficmanagement erlaubt.
Ein Thema bleibe aber ungeklärt und betreffe alle VPNs: Wie erzeugt und verwaltet der Admin eine möglichst simple SSL-PKI (Public Key Infrastructure) für viele User, ohne viel Geld für Dienstleister oder proprietäre Software in die Hand zu nehmen? Die Lösung sollte quelloffen sein, frei von Lizenzkosten und ähnlichen Komplikationen und bitte, bitte nicht Cloud- oder Webservice-basiert. Wie in den meisten Fällen sei es kein Problem, selbst signierte Zertifikate einzusetzen.
Die Hausmarke
Open VPN setzt dafür traditionell auf Easy RSA [2], dessen Version 2 weite Verbreitung findet. Viele Distributionen, allen voran die Debian-basierten, installieren es gleich zusammen mit »openvpn«. Eine Ausnahme ist Ubuntu, das »easy-rsa« erst ab Cosmic Cuttlefish (Version 18.10) mitbringt [3].
Der seit Jahren bereitstehende Nachfolger Easy RSA 3.0 macht ein paar Sachen einfacher und gar nicht so vieles anders [4], doch die meisten Lösungen und Setups basieren auch 2019 noch auf Version 2. Beiden Versionen ist eins gemeinsam: Sie kommen, für klickgewohnte Windows- und Apple-Anwender eher ungewohnt, ohne schickes GUI als reine Kommandozeilentools daher – offenbar ein Showstopper für viele Admins.
Listing 1
Open SSL from Hell
01 openssl x509 -req \ 02 -CA ca.crt \ 03 -CAkey ca.key \ 04 -set_serial 0x$(openssl rand -hex 16) \ 05 -days 3650 \ 06 -extfile <(echo -e "keyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage= serverAuth") \ 07 -in server.csr \ 08 -out server.crt
Open CA, XCA, Tiny CA: Es gibt und gab viele Open-Source-Tools, um die PKI-Verwaltung einfacher zu machen. Keines feiert echte Erfolge. Zu komplex scheint die Thematik selbst zu sein, zu fehleranfällig ein Workflow, den der Anwender, sei er nun Admin oder User, nicht verstanden hat.
Das erkannte offenbar auch die Firma Open VPN Limited von Open-VPN-Gründer James Yonan. Sie finanziert sich unter anderem über ein (Web-)Frontend, das es erlaubt, Zertifikate einfacher zu erzeugen und zu verteilen. Das betrifft aber nur das eigene Open-VPN-Produkt, das die Firma irgendwann als Open VPN 3.0 freigeben möchte.
Aus den Open-VPN-Seminaren nahm der Autor daher die Erkenntnis mit: Das erste Set an Server-, Client- und CA-Zertifikaten und -Schlüsseln zu erzeugen, ist die schwerste Aufgabe. Das gilt unabhängig vom eingesetzten Tool, denn am Ende werkelt stets Open SSL im Hintergrund mit Befehlszeilen wie in Listing 1.
Listing 2
»generate_certs_for_active_users.py«
01 #!/usr/bin/python
02 import urllib, json, os, subprocess, sys
03
04 url = "http://vpnapi.charite.de/vpn/user/status/activevpn"
05 response = urllib.urlopen(url);
06 data = json.loads(response.read())
07
08 for userentry in data:
09 if userentry.get('active'):
10 username = userentry.get('username')
11
12 if os.path.isfile( "/opt/openvpn/ca/keys/" + username + ".crt"):
13 # print "Cert for " + username + " exists"
14 a=1
15 else:
16
17 # get user details
18 detailsurl = "http://vpnapi.charite.de/vpn/user/details/" + username
19 response = urllib.urlopen(detailsurl);
20 detaildata = json.loads(response.read())
21 useremail = detaildata.get('mail')
22 if not useremail:
23 print "User " + username + " hat keine Emailadresse im LDAP"
24 # Import smtplib for the actual sending function
25 import smtplib
26
27 # Import the email modules we'll need
28 from email.mime.text import MIMEText
29
30 # Create a text/plain message
31 msg = MIMEText("User " + username + " hat keine Emailadresse im LDAP")
32
33 [...]
34 msg['Subject'] = 'User %s hat keine Emailadresse im LDAP' % username
35 msg['From'] = 'vpn@charite.de'
36 msg['To'] = 'vpn@charite.de'
37
38 [...]39 s = smtplib.SMTP('localhost')
40 s.sendmail('vpn@charite.de', 'vpn@charite.de', msg.as_string())
41 s.quit()
42 break
43
44 # end get user details
45
46 print "Need to generate cert for " + username
47 p = subprocess.Popen("/opt/openvpn/ca/build-key-auto " + username, \
48 bufsize=1, cwd="/opt/openvpn/ca", \
49 stdout=subprocess.PIPE, close_fds=False, \
50 universal_newlines=True, shell=True)
51 if p.wait:
52 ret = p.wait()
53 else:
54 ret = p.returncode
55 if (ret != 0):
56 # wenn nicht alles OK
57 if p.stdout:
58 print "Ausgabe war:"
59 print p.stdout.readlines()
60 else:
61 # alles OK!
62 print "Sende Zertifikat!"
63 p = subprocess.Popen("/opt/openvpn/scripts/send_certs_and_config.py " \
64 + username, bufsize=1, cwd="/opt/openvpn/ca", \
65 stdout=subprocess.PIPE, close_fds=False, \
66 universal_newlines=True, shell=True)
67 if p.wait:
68 ret = p.wait()
69 else:
70 ret = p.returncode
71 if (ret != 0):
72 if p.stdout:
73 print "Ausgabe war:"
74 print p.stdout.readlines()
75 # wenn nicht alles OK
Weil kein Admin diese Zeile regelmäßig eintippen will, vereinfacht eine Vielzahl von Anleitungen und Skripten den Prozess, ohne die Fehleranfälligkeit signifikant zu erhöhen. Auch Easy RSA ist im Prinzip nur ein intelligenter Open-SSL-Wrapper. Ob der dann auch für Tausende Anwender funktioniert, wollte der Autor von Freunden und Bekannten in der Linux- und Security-Welt wissen. Tut er, antworteten unter anderem die erfahrenen Sysadmins der Berliner Charité [5], Deutschlands größtem Krankenhaus. Der Abschnitt “Easy RSA 3” zeigt die zentralen Einrichtungsschritte.
Die Admins haben Easy RSA um ein paar Skripte erweitert und verwalten so aktuell 17 000 User. Ralf Hildebrandt, schon in der Vergangenheit eine hilfreiche Anlaufstelle für das Linux-Magazin, erklärte: “Wir nutzen Easy RSA auf dem VPN-Server und generieren die Zertifikate der Form »Username.vpn.charite.de« automatisch für die User. Scheidet ein Nutzer aus, widerrufen wir sie nach einer gewissen Grace-Periode. Die Login-Daten holen wir aus dem LDAP.”
Easy RSA 3
Auf Open Suse Tumbleweed installiert ein »zypper in easy-rsa« die Software in der Version 3.0. Gleiches erledigen Apt oder Aptitude, Yum oder der Paketmanager der Wahl auf anderen Distributionen. Hier kommt aber meist nur die Vorgängerversion zum Zug.
Nach der Installation liegen die Konfigurationsdateien im Verzeichnis »/etc/easy-rsa/«, alle folgenden Kommandos ruft der Admin in diesem Verzeichnis auf. In wenigen Schritten geht es so zur eigenen PKI. Zunächst initialisiert
easyrsa init-pki
die PKI und löscht gegebenenfalls alle zuvor eingegebenen Daten. Achtung, das führt zu einem kompletten Reset der PKI. Wer sich die Arbeit einfacher machen will, bearbeitet die Datei »vars« und reichert sie mit seinen Daten an – das spart später einige Tipperei.
Listing 3
send_certs_and_config.py (Teil 1)
01 #!/usr/bin/python
02 # -*- coding: utf-8 -*-
03 import urllib, json, subprocess, argparse, re, sys
04 import [...]
05
06 from M2Crypto import BIO, Rand, SMIME
07 from [...]
08
09 [...]
10 ssl_key = '/etc/ssl/private/mailcert-vpn.charite.de.key'
11 ssl_cert = '/etc/ssl/certs/mailcert-vpn.charite.de.crt'
12
13
14 def send_mail_ssl(sender, to, subject, text, files=[], attachments={}, bcc=[]):
15 """
16 Sends SSL signed mail
17 [...]
18 """
19
20 if isinstance(to, str):
21 to = [to]
22
23 # create multipart message
24 msg = MIMEMultipart()
25
26 # attach message text as first attachment
27 msg.attach( MIMEText(text, "plain", "utf-8") )
28
29 # attach files to be read from file system
30 for file in files:
31 part = MIMEBase('application', "octet-stream")
32 part.set_payload( open(file,"rb").read() )
33 Encoders.encode_base64(part)
34 part.add_header('Content-Disposition', \
35 'attachment; filename="%s"'
36 % os.path.basename(file))
37 msg.attach(part)
38
39 # attach files read from dictionary
40 for name in attachments:
41 part = MIMEBase('application', "octet-stream")
42 part.set_payload(attachments[name])
43 Encoders.encode_base64(part)
44 part.add_header('Content-Disposition', \
45 'attachment; filename="%s"' % name)
46 msg.attach(part)
47
48 # put message with attachments into into SSL' I/O buffer
49 msg_str = msg.as_string()
50 buf = BIO.MemoryBuffer(msg_str)
51
52 # load seed file for PRNG
53 Rand.load_file('/tmp/randpool.dat', -1)
54
55 smime = SMIME.SMIME()
56
57 # load certificate
58 smime.load_key(ssl_key, ssl_cert)
59
60 # sign whole message
61 p7 = smime.sign(buf, SMIME.PKCS7_DETACHED)
62
63 # create buffer for final mail and write header
64 out = BIO.MemoryBuffer()
65 out.write('From: %s\n' % sender)
66 out.write('To: %s\n' % COMMASPACE.join(to))
67 out.write('Date: %s\n' % formatdate(localtime=True))
68 out.write('Subject: %s\n' % subject)
69 out.write('Auto-Submitted: %s\n' % 'auto-generated')
70 out.write('X-Auto-Response-Suppress: %s\n' % 'OOF')
71
72 # convert message back into string
73 buf = BIO.MemoryBuffer(msg_str)
74
75 # append signed message and original message to mail header
76 smime.write(out, p7, buf)
77
78 # load save seed file for PRNG
79 Rand.save_file('/tmp/randpool.dat')
80
81 # extend list of recipents with bcc adresses
82 to.extend(bcc)
83
84 # finally send mail
85 p = Popen(["/usr/sbin/sendmail", "-fvpn@charite.de", "-oi", \
86 COMMASPACE.join(to)], stdin=PIPE, universal_newlines=True)
87 p.communicate(out.read())
Der Befehl »easy-rsa init-pki« erzeugt eine Certificate Authority (CA), »easyrsa gen-dh« einen Diffie-Hellman-Key für den initialen Schlüsseltausch zwischen einander unbekannten, nicht vertrauenswürdigen Partnern. Der Befehl
easyrsa build-client-full Eindeutiger_Name_des_Client-Zertifikates_(DN) nopass
erzeugt ein Client-Zertifikat, das ohne Passwort Zugriff auf das VPN geben soll. Steht dort etwa »easyrsa build-client-full Client234 nopass« taucht im Client-Zertifikat »Client234« als DN-Eintrag auf (Distinguished Name), was Open VPN für zahlreiche praktische Funktionen verwendet. Analog dazu erzeugt
easyrsa build-server-full Eindeutiger_Name_des_Client-Zertifikates_(DN) nopass
ein Serverzertifikat ohne Passwortschutz.Jetzt findet der Anwender Zertifikate und private sowie öffentliche Diffie-Hellman-Schlüssel in den Verzeichnissen unter »/etc/easy-rsa/pki« (Abbildungen 1 und 2). Nicht zuletzt weil diese Ordner alle privaten Keys aller Zertifikate enthalten, sollte der Admin diese PKI-Struktur vom Open-VPN-Server trennen. Idealerweise läuft Easy RSA auf einem Inselsystem ohne Verbindung mit dem Internet. Der Wunsch mag archaisch anmuten und sich nicht immer in der Praxis umsetzen lassen. Doch ein Verlust des privaten Schlüssels der PKI (»ca.key«) öffnet die gesamte Infrastruktur für Angreifer.
Listing 4
send_certs_and_config.py (Teil 2)
01 # BEGINNING OF SCRIPT
02 if not os.geteuid() == 0:
03 sys.exit('Script must be run as root')
04
05 # Commandline arguments parsen
06 parser = argparse.ArgumentParser(description='versendet \
07 VPN-Konfigurationsdatei an Nutzer')
08
09 parser.add_argument('userinput', help='<username|Mailadresse>')
10 parser.add_argument('mailadresse', nargs='?', help='<Ziel-Mailadresse \
11 - default ist Antrags-Mailadresse des Nutzers>')
12 args = parser.parse_args()
13
14 [...]
15
16 is_mail = re.compile('^.+?\@.+')
17 if is_mail.match(args.userinput):
18 #print args.userinput + " ist eine Emailadresse"
19 url = "http://vpnapi.charite.de/vpn/user/detailsbyemail/" \
20 + args.userinput
21 else:
22 #print args.userinput + " ist ein Username"
23 url = "http://vpnapi.charite.de/vpn/user/details/" \
24 + args.userinput
25
26 #response = urllib.urlopen(url);
27 response = urllib.urlopen(url, proxies={});
28 data = json.loads(response.read())
29
30 username = data.get('username')
31 active = data.get('vpnActive')
32
33 if args.mailadresse:
34 if is_mail.match(args.mailadresse):
35 #print args.mailadresse + " ist eine Emailadresse"
36 email = args.mailadresse
37 else:
38 print args.mailadresse + " ist keine Emailadresse!"
39 sys.exit(1)
40 else:
41 email = data.get('mail')
42
43 if not username:
44 print "Username existiert nicht"
45 sys.exit(1)
46
47 if not active:
48 print "User ist nicht aktiv"
49 sys.exit(1)
50
51 if not email:
52 print "Email ist leer"
53 sys.exit(1)
54
55 # Emailadresse codieren, wenn nicht leer!
56 email = email.encode('ascii','ignore')
57
58 #print username, email
Übrigens: »easyrsa export-p12 Zertifikat-DN« exportiert ein Schlüsselpaar ins PKCS12-Format. Über »easyrsa gen-req DN« und »easyrsa sign-req client DN« sowie die Parameter »import-req« und »show-req« bietet Easy RSA ein vollständiges PKI-Management. Das Request- und Sign-Verfahren übernehmen die Befehle »build-client-full« und »build-server-full« automatisch, ohne dass sie Eingriffe des Admin benötigen.
Die Skripte der Charité basieren auf Easy RSA 2.2.2-2, das die in Berlin bevorzugte Debian-Version mitliefert. Sie lassen sich aber recht schnell für die Version 3 umstricken. Weitere Details lieferte Ralf Hildebrandt: “Unser Skript heißt »/opt/openvpn/scripts/generate_certs_for_active_users.py« (Listing 2) und generiert ab den Zeilen 47 und 63 für die aktiven User im VPN-System ein Zertifikat, wenn noch keins vorhanden ist. In »/etc/easy-rsa/« findet »ll *.crt | wc -l« derzeit 16849 Userzertifikate.”
Die kleine Sammlung umfasst zwei längere und drei kurze Skripte, die das Linux-Magazin hier in Auszügen abdruckt. Die kompletten Listings gibt es online unter [6]:
- »generate_certs_for_active_users.py« erzeugt ein Zertifikat für alle existierenden User ohne Zertifikat (Listing 2).
- »send_certs_and_config.py« (Listing 3 bis Listing 5) übernimmt das Signieren mit S/Mime und das Versenden via Mail an die VPN-Anwender.
- »revoke_remove_cert_without_user« (Listing 6) kommt zum Einsatz, wenn ein User ausscheidet.
- Listing 6 benutzt »checkCertWithoutUser.pl« unter [6] und »revoke_and_delete« (Abbildung 3), um nach einiger Zeit Zertifikate zu entsorgen.
Im nächsten Schritt folgt ein detaillierter Blick auf die hier erwähnten Listings.
Certs für aktive User
Um für die aktiven User im VPN-System ein Zertifikat zu erzeugen, fragt »generate_certs_for_active_users.py« (Listing 2) über das Json-API ab, welche Anwender aktiv sind. Es iteriert über diese User und prüft, ob in »/opt/openvpn/ca/keys« ein ».crt«-file mit dem Namen des jeweiligen Users vorliegt. Ist das nicht der Fall, generiert es ein Zertifikat.
Listing 5
send_certs_and_config.py (Teil 3)
01 if os.path.isfile( "/opt/openvpn/ca/keys/" + username + ".crt"):
02 #print "Cert for " + username + " exists"
03
04 # Textteil der Mail einlesen
05 with open('/opt/openvpn/etc/sendcertagain-mailbody.txt', 'r') as myfile:
06 text=myfile.read()
07
08 # Dateinamen für das Attachment festlegen
09 configfilename="charite-" + username + ".ovpn"
10
11 # Pfade des Schlüsselmaterials festlegen
12 keyfilename="/opt/openvpn/ca/keys/" + username + ".key";
13 crtfilename="/opt/openvpn/ca/keys/" + username + ".crt";
14 cafilename="/opt/openvpn/ca/keys/ca.crt";
15
16 # Schlüsselmaterials öffnen
17 with open('/opt/openvpn/etc/charite.ovpn.template', 'r') as myfile:
18 configtemplatefile = myfile.read()
19 with open(cafilename, 'r') as myfile:
20 cafile = myfile.read()
21 with open(keyfilename, 'r') as myfile:
22 keyfile = myfile.read()
23 with open(crtfilename, 'r') as myfile:
24 crtfile = myfile.read()
25
26 sender = str(Header("Charité VPN-Administrator", "utf8")) + " <vpn@charite.de>"
27 subject = Header("Konfigurationsdatei zu Ihrem Charité VPN-Zugang","utf8")
28
29 configfile = configtemplatefile + "<ca>\n" + cafile + "</ca>\n<key>\n" \
30 + keyfile + "</key>\n<cert>\n" + crtfile + "</cert>\n"
31 send_mail_ssl(sender, email, subject, text, files=[], \
32 attachments={configfilename: configfile} )
33 print "Konfigurationsdatei wurde an " + email + " geschickt."
34
35 else:
36 print "Der User " + username + " hat kein Zertifikat"
37 sys.exit(1)
Dies erledigt das mit Easy RSA gelieferte Skript »/opt/openvpn/ca/build-key-auto username«. Dann schickt »send_certs_and_config.py« (Listings 3 bis 5) die Zertifikate via E-Mail signiert an die Anwender. Der VPN-Server muss dazu nur die CA erkennen, die PKI-Arbeit kann und sollte anderswo erfolgen.
Auf Widerruf
Wenn Mitarbeiter den Arbeitgeber verlassen, muss der Admin sicherstellen, den VPN-Zugriff zu unterbinden. Bei der Charité erledigen das ein Tool in Listings 6 und das Perl-Skript »checkCertWithoutUser.pl« [6], wobei sie die Zertifikate erst nach einer Übergangszeit von drei Monaten irreversibel löschen. “Oft kommen User binnen dreier Monate wieder. In dem Fall will man denen nicht die Bürde auferlegen, eine neue Konfiguration oder neue Zertifikate installieren zu müssen”, erklärt Hildebrandt.
Listing 6
revoke_remove_cert_without_user
01 #!/bin/sh
02 /opt/openvpn/scripts/checkCertWithoutUser.pl | xargs --no-run-if-empty --replace /opt/openvpn/scripts/revoke_and_delete {}
Das Skript »revoke_remove_cert_without_user« (Listing 6) generiert via »checkCertWithoutUser.pl« eine Liste von Zertifikaten, zu denen aktive User fehlen und pipt diese an »revoke_and_delete« (Abbildung 3). Erst das nutzt Easy RSA fürs Widerrufen und Löschen des Schlüsselmaterials.
Geht doch!
Dass das System der Charité mittlerweile 17000 Nutzer verwaltet, überraschte laut Hildebrandt selbst die Admins – so problemlos und stabil läuft Easy RSA im professionellen Betrieb. “Der Vorteil von Easy RSA liegt ganz klar in der Stabilität: Das Ding macht einfach 100-prozentig und zuverlässig genau das, was man ihm sagt. In den über zehn Jahren Betrieb ist uns das noch nie um die Ohren geflogen und bietet genau die High-Level-Kommandos, die wir brauchen, um Zertifikate zu erzeugen und zurückzuziehen.”
Die so generierten Konfigurationen funktionieren auch mit mobilen Endgeräten und dem handlichen Open-VPN-Format der Embedded Keys. Da lassen sich Konfiguration, Zertifikate und Keys direkt in der Konfigurationsdatei einfügen, ohne Verweis auf andere Dateien. Damit haben Anwender nur eine Konfigurationsdatei für den Zugang – was die Akzeptanz deutlich steigert. Das funktioniert problemlos mit modernen Smartphones.
Dass die Private Keys nicht passwortgeschützt sind, ist für das Szenario weniger kritisch: “Der Passwortschutz beim Zugang gelingt über die LDAP-Authentifizierung, die ans Active Directory gekoppelt ist. Also muss jeder User ohnehin sein Passwort eingeben, wenn er sich anmeldet. Das ist zwar für die User das am meisten genannte Ärgernis, aber notwendig”, erklärt Hildebrandt.
Hinzu komme, dass weder Android noch iOS einen Webproxy via Autoconfig gestatten. “Unsere User können VPN nutzen, aber der Haupteinsatzzweck ist das Surfen im Internet über unsere Proxys, weil die Anwender darüber vollen Zugang zu den wissenschaftlichen Journalen und Papers bekommen.” Bei Chrome OS lasse sich genau ein Proxy für die eine VPN-Verbindung einstellen.
Infos
- Open VPN: https://openvpn.net
- Easy RSA: https://github.com/OpenVPN/easy-rsa
- Easy RSA für Ubuntu: https://packages.ubuntu.com/search?keywords=easy-rsa&searchon=names&suite=all§ion=all
- Easy RSA 3.0 Howto: https://community.openvpn.net/openvpn/wiki/EasyRSA3-OpenVPN-Howto
- Charité Berlin: https://www.charite.de
- Komplette Listings zum Artikel: https://www.linux-magazin.de/static/listings/magazin/2019/08/easy_rsa/











in den Listings wird immer ein Ordner “/opt/openvpn” angesprochen. Was is da genau drin und wo kommt der her? Die funktion “/opt/openvpn/ca/build-key-auto” würde mich dabei interessieren weil die ist nicht in den Listings vorhanden.
Danke!