Auth-Gateway selbstgemacht
Eine regelmäßige Änderung von Kennwörtern ist unter gängigen Betriebssystemen kein Problem. Aber da gibt es ja auch noch SSH-Keys um sich an Servern anzumelden. Auch ein SSH-Key kann natürlich mit einem Kennwort geschützt werden. Der Server an dem man sich damit anmeldet kann aber weder prüfen ob der Key mit einem Kennwort geschützt ist, noch wann dieses gesetzt wurde. Es mag eine Spitzfindigkeit sein, aber gerade die machen ja oft besonders Spaß. In vielen Dokumenten zum Datenschutz wird eine regelmäßige Änderung von Kennwörtern gefordert, ist das bei einem SSH-Key der Fall? Und wie stellt der Admin das sicher?
Geht ein private SSH-Key (schlimmstenfalls ohne Kennwort) verloren kann er so lange von einem Angreifer benutzt werden, bis der entsprechende Public Key auf dem Zielsystem entfernt wird. Alle 90 Tage den public SSH-Key auf allen Servern zu entfernen und neue Keys von allen Nutzern anzufordern und zu deployen ist wenig praktikabel.
Stattdessen kann man signierte SSH-Keys verwenden. Das Prinzip dürfte von SSL-Zertifikaten bekannt sein. Es gibt eine Certificate Authority (CA), welche einen kryptographischen Schlüssel signiert. Systeme die der CA vertrauen akzeptieren diese Signatur. Die Signatur ist außerdem nur für eine bestimmte Zeit gültig. Das eigentliche SSH-Schlüsselpaar ist hierbei egal und muss im Grunde nie verändert werden. Wenn die Signatur abgelaufen ist kann man sie einfach erneuern.
Um signieren zu können müssen wir uns zunächst eine CA erzeugen:
ssh-keygen -f users_ca
Der Benutzer muss uns nun den Public Key seines, wie gewohnt selbst erzeugten und ggf. bereits bestehenden, SSH-Keypaars zukommen lassen. Dieser wird dann wie folgt signiert:
ssh-keygen -s users_ca -I user_username -n username -V +90d id_rsa.pub
Erzeugt wird darauf eine id_rsa-cert.pub
, diese wird dem Benutzer ausgehändigt und zum bestehenden SSH-Key kopiert (z.B. ~/.ssh/
).
Damit ein Server nun den signierten Key nutzt ist nun noch eine kleine Änderungen an der SSH-Konfiguration nötig. Bei der Erstellung unserer CA wurde neben dem Key zur Signatur auch die Datei users_ca.pub
erzeugt. Diese laden wir nun auf alle Server hoch (/etc/ssh/users_ca.pub
), die das neue Verfahren nutzen sollen und fügen die folgende Zeile in /etc/ssh/sshd_config
ein:
TrustedUserCAKeys /etc/ssh/users_ca.pub
Nach einem Neustart von SSH werden signierte SSH-Key akzeptiert. Allerdings werden auch weiterhin unsignierte Keys angenommen, sofern diese in der Datei ~/.ssh/authorized_keys
des jeweiligen Benutzers hinterlegt sind. Um dies zu umgehen ändern wir kurzerhand den Eintrag für das AuthorizedKeysFile in unserer SSH-Konfiguration:
AuthorizedKeysFile /etc/ssh/authorized_keys
Es ist auch möglich dies nur für bestimmt Gruppen von Benutzern zu aktivieren:
Match Group developers
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile /etc/ssh/authorized_keys
Nach dieser Änderung (SSH Neustart nicht vergessen) werden nur noch unsignierte Keys akzeptiert die sich in /etc/ssh/authorized_keys
befinden und für die dementsprechend kein Benutzer Schreibrechte haben sollte.
Ab sofort können sich Benutzer also nur noch dann mit einem Key anmelden wenn dieser signiert ist. Ein Eintrag in authorized_keys
ist nicht mehr nötig, da der Server alle Keys akzeptiert die mit unserer CA signiert wurden.
Im obigen Beispiel für die Gruppe developers wurde zudem mittels PasswordAuthentication no auch die Möglichkeit abgeschaltet sich mit einem normalen Kennwort anzumelden. Der Zugang zum System ist also ausschließlich mit einem signierten SSH-Key möglich. Dieser kann auch nur einen Tag gültig sein, z.B. um einem externen Techniker einen Wartungszugang zu geben.
Die möglicherweise dumme Idee, den Signaturvorgang automatisieren.
Hat man bereits einen LDAP-Server oder ein ActiveDirectory (in dem regelmäßige Kennwortänderungen erzwungen wird) könnte man z.B. einfach einen Apache mit einer LDAP-Authentifizierung ausstatten. Meldet sich ein Benutzer erfolgreich an LDAP an kann er per Formular seinen public Part des SSH-Keys hochladen. Dieser wird dann von der CA signiert und der Benutzer erhält seine id_rsa-cert.pub
zurück. Die Signatur kann bei solch einer Automatisierung natürlich bedeutend kürzer sein als 90 Tage.
Nachteil einer solchen Automatisierung: Der Key der CA kann nicht mit einem Kennwort geschützt werden, bzw. muss dieses im Speicher abgelegt werden (z.B. als Umgebungsvariable), da der Prozess sonst natürlich bei der Kennworteingabe stehen bleiben würde. Wer also Zugang zu dem Server mit dem CA-Key hat kann sich beliebige Keys signieren lassen. Damit bekommt man zwar noch lange nicht Zugang zu jedem Server, da man auch noch immer einen passenden Benutzer auf dem Server benötigt (der auch im signierten Key festgeschrieben ist), aber man sollte diese Gefahr auch nicht unerwähnt lassen.
Hier mal ein Proof of Concept:
Der “Client” um die signierte Keydatei abzurufen:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import requests
import getpass
import requests.packages.urllib3
from os.path import expanduser
import subprocess
import sys
import os
requests.packages.urllib3.disable_warnings()
username = getpass.getuser()
print "Please enter the LDAP password for "+username+"..."
password = getpass.getpass()
home = expanduser("~")
url = 'https://ldap.internal/ldap-auth/sign.php'
pubkey = {'pubkey': open(home+'/.ssh/id_rsa.pub', 'rb')}
response = requests.post(url, files=pubkey, auth=(username, password), verify=False)
if response.status_code == 200:
f = open(home+'/.ssh/id_rsa-cert.pub','w')
f.write(response.content)
f.close()
print "Got new certificate "+home+"/.ssh/id_rsa-cert.pub..."
os.system("ssh-keygen -L -f "+home+"/.ssh/id_rsa-cert.pub")
print "Signed key was saved. You may now log in."
else:
print "ERROR: " + str(response.status_code)
print response.content
Und hier die entsprechende Serverkomponente:
<?php
$username = $_SERVER["AUTHENTICATE_SAMACCOUNTNAME"];
$uploaddir = '/var/www/ldap-auth/';
$uploadfilename = $username . basename($_FILES['pubkey']['name']);
$uploadfile = $uploaddir . $uploadfilename;
$cafile = '/var/users_ca';
$certfilename = str_replace('.pub','-cert.pub',$uploadfilename);
$certfile = $uploaddir . $certfilename;
if (move_uploaded_file($_FILES['pubkey']['tmp_name'], $uploadfile)) {
system('ssh-keygen -s '.$cafile.' -n '.$username.' -V +1d -I '.$username.'_key '.$uploadfile);
passthru('cat '. $certfile);
} else {
var_dump(http_response_code(500));
echo "Error during file upload!\n";
}
unlink($uploadfile);
unlink($certfile);
?>
Die zugehörige Apache-Konfiguration:
<Location /ldap-auth>
<RequireAny>
AuthType Basic
AuthBasicProvider ldap
AuthName "Password protected. Enter your AD username and password."
AuthLDAPURL "ldap://ldap.internal/CN=Users,DC=dom,DC=internal?sAMAccountName?sub?(objectClass=*)"
AuthLDAPBindDN "ldap@dom.internal"
AuthLDAPBindPassword CHANGEME
Require valid-user
</RequireAny>
</Location>