NGINX als SSL IMAP Proxy mit Benutzereinschränkung

24. Juni 2009 | Von | Kategorie: Admin

Heute habe ich mich wieder mit einem typischen Problem rumgeschlagen. Ein Kunde betreibt im Intranet eine OpenSource Groupware, die wir für ihn aufgesetzt haben. Nun möchten einige Anwender gerne von außen mit ihren mobilen Geräten auch auf die E-Mails zugreifen. Allerdings haben wir Bauchschmerzen, diesen Zugang allen Benutzer bereitzustellen. Die Kennwortrichtlinien werden nicht besonders gut bei dem Kunden umgesetzt. Ein Wörterbuchangriff auf den Zugang hätte daher wahrscheinlich sehr schnell Erfolg. Daher soll der Zugang nur bestimmten Benutzern erlaubt werden. Perspektivisch soll auch der Zugriff auf die Weboberfläche und der Versand von E-Mails per SMTP diesen Benutzern gestattet werden. Die Benutzer werden alle in einem OpenLDAP-Baum gepflegt. Auch die Einstellung, ob ein Benutzer von außen zugreifen darf sollte hier verwaltet werden.

Lösung: NGINX !

Installiert wurde die aktuellste Version NGINX. Diese wurde so übersetzt, dass auch das Mail-SSL-Modul unterstützt wird. Anschließend wurde folgende Konfigurationsdatei in Anlehnung an die vorhandenen Beispiele im Netz erzeugt:

user  nginx;
worker_processes  1;

events {
worker_connections  1024;
}

http {
perl_modules  perl/lib;
perl_require  ldapauth.pm;

server {
listen 127.0.0.1:8000;
location /auth {
perl  ldapauth::handler;
}
}
}

mail {
auth_http  127.0.0.1:8000/auth;

imap_capabilities  "IMAP4rev1"  "UIDPLUS";

server {
ssl on;
ssl_certificate /etc/nginx/nginx.pem;
ssl_certificate_key /etc/nginx/nginx.pem;
#starttls only;
#listen     143;
listen     993;
protocol   imap;
proxy      on;
}
}

Hiermit startet der NGINX auf zwei Ports je einen Listener. Auf dem Port 993 nimmt er von außen IMAPS-Anfragen entgegen. Um den Benutzer zu authentifizieren, greift er auf eine HTTP-URL zurück. Dort sendet er die Anfrage, ob der Benutzer authentifiziert ist und erwartet eine entsprechende Antwort. Das Anfrage sieht in etwa so aus:

GET /auth HTTP/1.0
Host: mail.example.com
Auth-Method: plain
Auth-User: ralf
Auth-Pass: kennwort
Auth-Protocol: imap
Auth-Login-Attempt: 1
Client-IP: 192.168.1.1

Er erwartet dann die folgende Antwort:

HTTP/1.0 200 OK
Auth-Status: OK
Auth-Server: 192.168.0.3
Auth-Port: 143

Diese HTTP-Anfrage sende ich an http://localhost:8000/auth. Dort läuft der zweite Listener von NGINX als Webserver. Dieser lädt ein Embedded-Perl-Modul, welches als
Handler für die Routine aufgerufen wird. Dieses Modul verbindet sich mit dem LDAP-Server und sucht dort nach dem Benutzer. Damit nicht jeder Benutzer diesen Zugang nutzen kann, sucht das Modul nur nach Benutzern, die gleichzeitig ein weiteres Attribut gesetzt haben. Hierfür habe ich eine LDAP-Schema-Erweiterung durchgeführt:

Das LDAP-Schema sieht so aus:


attributetype ( 1.3.6.1.4.1.29068.0.1
NAME 'externalImapAccess'
DESC 'May the user access the IMAP-server externally'
EQUALITY caseIgnoreMatch
Syntax 1.3.6.1.4.1.1466.115.121.1.15)

objectclass ( 1.3.6.1.4.1.29068.1.1
NAME 'externalUser'
DESC 'external access allowed to user'
SUP top AUXILIARY
MAY    ( externalImapAccess ))

Nur wenn der Benutzer auch das Attribut externalImapAccess auf den Wert true gesetzt hat, wird er bei der Suche durch das Modul gefunden. Anschließend prüft das Modul, ob der Benutzer auch das richtige Kennwort eingegeben hat, indem es sich am LDAP-Server mit diesem Kennwort anmeldet. Nur dann gibt das Modul die Informationen über den echten IMAP-Server zurück und NGINX baut die Verbindung zu diesem Server als Proxy mit den übergebenen Benutzer und Kennwort auf.

Hier ist das LDAP-Modul ldapauth.pm (Dummerweise werden die spitzen Klammern ersetzt in der Darstellung. Ein Zugriff über ViewSource am oberen Rand des Listings zeigt den Code aber richtig an.):


package ldapauth;
use nginx;
use Net::LDAP;

our $auth_ok;

sub handler {
my $r = shift;
$ldap = Net::LDAP->new("ldap.example.de") or die "$@";
$auth_ok=0;

$user=$r->header_in("Auth-User");
$pass=$r->header_in("Auth-Pass");
# Suche nach dem Benutzer
$mesg = $ldap->bind ( version => 3 );
my $result = $ldap->search ( base    => "dc=example,dc=de",
scope   => "sub",
filter  => "&(objectclass=*)(uid=$user)(externalImapAccess=true)",
attrs   =>  "uid"
);
if ($result->count==1) {
my @entries = $result->entries;
my $dn = $entries[0]->dn;

$mesg = $ldap->bind ( "$dn", password => $pass, version => 3 );
if ($mesg->code==0) {
$auth_ok=1;
$r->header_out("Auth-Status", "OK") ;
$r->header_out("Auth-Server", "192.168.0.3");
$r->header_out("Auth-Port", "143");
}
}
if ($auth_ok==0) {
$r->header_out("Auth-Status", "Invalid login or password") ;
}

$r->send_http_header("text/html");

return OK;
}

1;
__END__

Nun kann über das LDAP-Attribut bequem der Zugriff über den Proxy per IMAPS verwaltet werden. Dieser muss nun nur noch in der Firewall erlaubt werden. Besitzt ein Benutzer das Attribut externalImapAccess mit dem Wert true, darf er zugreifen. Weist das Attribut den falschen Wert auf oder existiert es nicht, ist der Zugriff nicht erlaubt.

Post to Twitter Post to Yahoo Buzz Post to Delicious Post to Digg Post to Facebook Post to Ping.fm Post to Reddit

Tags: | | | | |

2 Kommentare
Hinterlasse einen Kommentar »

  1. Hi,

    erstmal, eine super Doku / Anleitung!

    Leider scheint Nginx in Antwortheader zwingend eine IP Adresse als Upstream zu erwarten, schade das man hier keine Hostnamen verwenden kann, oder doch?

    Gruß,
    Andreas

  2. Hallo Andreas,

    das ist durchaus möglich. Ich habe es nie mit einem DNS-Namen versucht. Die offizielle NginX-Doku nutzt auch nur eine IP-Adresse. Wofür brauchst Du einen DNS-Server? Theoretisch kannst Du die DNS-Auflösung ja auch z.B. im ldapauth.pm Modul durchführen, oder? Dann lieferst Du einfach die entsprechende IP-Adresse nur aus, speicherst aber im LDAP-Server den DNS-Namen.

    Gruß,

    Ralf

Schreibe einen Kommentar

Fühle dich ermuntert einen Kommentar, Anmerkungen, Hinweise oder deine Ideen zum Thema zu hinterlassen. Wir freuen uns über deine Rückmeldung.