Gruppen-Authentifizierung für Nginx

Nginx hat kein Äquivalent für Apaches gruppenbasierte Zugriffskontrolle. Dieses Apache-Feature kann aber mit etwas Scripting leicht emuliert werden.

Damit der Browser beim Zugriff auf bestimmte Webressourcen einen hässlichen Dialog zur Abfrage von Usernamen und Passwort öffnet, muss man in nginx eine Konfiguration wie die Folgende erstellen:

1
2
3
4
location /protected {
    auth_basic "Mein kleiner geheimer Dienst";
    auth_basic_user_file /pfad/zu/auth/htusers;
}

Die Passwortdatei /pfad/zu/auth/htusers kann entweder mit dem Apache-Tool htpasswd oder dem Python-Skript htpasswd.py erzeugt werden. Das Ergebnis sieht so aus:

1
2
3
4
5
6
$ cat /pfad/zu/auth/htusers
hihosilver:$apr1$7iHB6eAM$bhrCoWXyXVDHXYo4aXZlf.
tom:$apr1$k03jYOif$hcf8rXosgreIz0v5oAtqD0
dick:$apr1$ZtrDNWP4$1G7sOMKOGZuBYdCu37bSX.
harry:$apr1$Lv1OSEIx$HHNvUurRBs/IRtSuYGXMh/
superuser:$apr1$wh3lz3Zw$TW4BO3mhEcWkFwcx.fZ760

Die äquivalente Apache-Konfiguration wäre:

1
2
3
4
5
6
<Location /protected>
    AuthType Basic
    AuthName "Mein kleiner geheimer Dienst"
    AuthUserFile "/pfad/zu/auth/htusers"
    Require valid-user
</Location>

Die Anweisung Require valid-user bedeutet dabei, alle “Benutzer in AuthUserFile”.

Gruppenbasierte Authentifizierung, die simple Methode

Jetzt wird ein weiterer geschützter Bereich /protected/admin erforderlich, der nur für die Benutzer hihosilver und superuser zugänglich sein soll. Mit Apache ist das trivial:

1
2
3
4
5
6
7
<Location /protected/admin>
    AuthType Basic
    AuthName "Mein kleiner geheimer Dienst"
    AuthUserFile "/pfad/zu/auth/htusers"
    AuthGroupFile "/pfad/zu/auth/htgroups"
    Require group admin
</Location>

In Zeile 5 wird eine Gruppendatei eingeführt, und nur Benutzern aus der Gruppe admin wird Zugriff auf diesen Bereich gewährt. Die Gruppendatei kann mit einem beliebigen Texteditor erstellt werden:

1
2
3
$ cat /pfad/zu/auth/htgroups
admin: superuser hihosilver
users: superuser hihosilver tom dick harry

Nur superuser und hihosilver gehören der Gruppe admin an, die zum Zugriff auf /protected/admin berechtigt ist.

Da nginx jedoch keine Gruppen kennt, bleibt nichts anderes übrig, als eine zweite Passwortdatei zu erstellen:

1
2
3
4
location /protected/admin {
    auth_basic "Mein kleiner geheimer Dienst";
    auth_basic_user_file /pfad/zu/auth/admin.users;
}

Die Konfiguration entspricht exakt der für den Bereich /protected. Es wird aber eine andere Passwortdatei verwendet (line 4), die sich zum Beispiel so erzeugen ließe:

1
2
$ cd /pfad/zu/auth
$ grep 'hihosilver|superuser:' htusers >admin.users

Damit wird die Passwortinformation für die Benutzer hihosilver und superuser kopiert. Und leider auch für alle anderen Benutzer, die einen dieser Strings im Passwort-Digest oder auch im Benutzernamen enthalten. Der reguläre Ausdruck muss noch etwas verfeinert werden:

1
2
$ cd /pfad/zu/auth
$ grep -E '^(hihosilver|superuser):' htusers >admin.users

Für viele einfache Szenarien ist das bereits ausreichend, und es lohnt sich nicht, den Mechanismus zu verbessern. Es sollte jedoch darauf geachtet werden, dass in allen Konfigurationen der gleiche Authentifizierungs-Bereich (“realm”, das ist der Wert für auth_basic) verwendet wird. Anderenfalls werden auch die Admin-Benutzer beim Wechsel in den Admin-Bereich erneut zur Passworteingabe aufgefordert.

Gruppenbasierte Authentifizierung, die komfortable Methode

Die simple Lösung mit grep wird irgendwann unpraktisch, wenn es viele verschiedene Gruppen oder geschützte Bereich gibt. Hier wäre eine Gruppendatei à la Apache dann doch hilfreich. Zum Glück ist das nicht sehr schwer.

Um eine vergleichbare Lösung für nginx zu haben, muss das Skript nginx-groups.pl heruntergeladen und im Verzeichnis /path/to/auth. Dann muss die Gruppendatei htgroups (siehe oben) erzeugt werden, und das Skript ausgeführt werden:

1
2
3
$ perl nginx-groups.pl htusers htgroups 
Writing users file 'admin.users'.
Writing users file 'users.users'.

Das Skript liest die Dateien mit den Benutzer- und Gruppeninformationen ein und erzeugt daraus eine Datei GRUPPE.users für jede Gruppe GRUPPE.

Das Skript ist ziemlich primitiv und eingeschränkt. Zum Beispiel wird immer ins aktuelle Verzeichnis geschrieben, und das Namensschema GRUPPE.users ist auch hartkodiert.

Wer den Ehrgeiz dazu hat, kann das Skript gerne aufbohren oder an individuelle Bedürfnisse anpassen. Es ist einfach zu verstehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#! /usr/bin/env perl

use strict;

die "Usage: $0 USERSFILE GROUPSFILE\n" unless @ARGV == 2;
my ($users_file, $groups_file) = @ARGV;

my %users;
open my $fh, "<$users_file" or die "cannot open '$users_file': $!\n";
while (my $line = <$fh>) {
    chomp $line;
    my ($name, $password) = split /:/, $line, 2;
    next if !defined $password;
    $users{$name} = $line;
}

open my $fh, "<$groups_file" or die "cannot open '$groups_file': $!\n";
while (my $line = <$fh>) {
    my ($name, $members) = split /:/, $line, 2 or next;
    next if !defined $members;
    $name =~ s/[ \t]//g;
    next if $name eq '';
    my @members = grep { length $_ && exists $users{$_} } 
                  split /[ \t\r\n]+/, $members;
    
    my $groups_users_file = $name . '.users';

    print "Writing users file '$groups_users_file'.\n";

    open my $wh, ">$groups_users_file" 
        or die "Cannot open '$groups_users_file' for writing: $!\n";

    foreach my $user (@members) {
        print $wh "$users{$user}\n"
            or die "Cannot write to '$groups_users_file': $!\n";
    }

    close $wh or die "Cannot close '$groups_users_file': $!\n";
}

In den Zeilen 8 bis 15 wird die Benutzerdatei zeilenweise eingelesen. Jede passende Zeile wird in einem Hash (assoziativen Array) unter dem Benutzernamen abgespeichert.

Danach wird die Gruppendatei auf die gleiche Weise gelesen und in Zeile 30 für jede Gruppe eine eigene Passwortdatei geschrieben. Es ist wichtig, dass diese Passwortdatei auch geschrieben wird, wenn die die Gruppe keine Mitglieder hat, weil nginx ansosten nicht mehr aktuelle Dateien verwendet und eventuell ungewollten Zugriff erlaubt.


blog comments powered by Disqus