NginX Basic-Auth mit Datenbank-Backend

Security Webserver
21.08.2019 | Andreas Müller

Vor einigen Jahren bin ich von Apache zu NginX umgestiegen, da ich meine Webseiten in Docker-Container verpackt hatte und der WebServer zum Proxy für den jeweiligen Container wurde.
Hier hat NginX einfach die Nase vorne und glänzt durch eine deutlich einfachere Konfiguration. Zudem lassen sich auch andere Streams durchleiten und nebenbei mittels SSL Zertifikat absichern (z.B. ein TCP-Stream).

Basic-Auth für NginX

Unter Apache ist die Absicherung von Verzeichnissen mittels Basic-Auth sehr einfach zu realisieren, wenn eine .htaccess-Datei verwendet wird. Eine andere Art habe ich damals nie benutzt.
Diese Authentifizierung ist auch unter NginX relativ einfach möglich, benötigt jedoch die Installation der apache2-utils.

Installation htpasswd und erzeugen einer htpasswd Datei:

# apt-get install apache2-utils
# htpasswd /etc/nginx/auth/htpasswd $user

Mit dieser Datei kann nun die Einrichtung stattfinden: z.B. /etc/nginx/sites-enabled/default

server { # ... sonstige Einrichtung des Server-Bereichs location /data { # ... Einrichtung des Ordners # Hier wird Basic-Auth aktiviert (und das Realm gesetzt) auth_basic "Restricted Area"; # Hier wird angegeben, wo die Benutzer und Passwörter gespeichert sind auth_basic_user_file /etc/nginx/auth/htpasswd; location /public { # Auf diesen Ordner dürfen wieder alle zugreifen auth_basic off; } } }

Mehr Flexibilität mit Lua

Zur Erweiterung der Serverlogik mit komplexeren Abfragen steht in NginX ein Lua-Modul zur Verfügung. Dies muss vermutlich noch installiert werden:

# apt-get install libnginx-mod-http-lua lua-nginx-string

Im Anschluss kann die Konfiguration so geändert werden, dass ein Lua-Script die Basic-Auth Prüfung übernimmt.

server { # ... location /restricted { # Hier wird das Realm gesetzt set $lua_realm 'Restricted Area'; # Wir definieren noch eine bestimmte Gruppe für die spätere Abfrage set $lua_group 'restricted group'; # Hier wird die Authentifizierung an das Lua-Script übergeben access_by_lua_file /etc/nginx/auth/auth.lua; # ... restliche Einrichtung } }

Wer nun der Sprache Lua mächtig ist, kann hergehen und mittels Lua eine vollständige Authentifizierung auf Basis von Basic-Auth vornehmen.
Ich für meinen Teil wollte gerne einen Benutzer-Abgleich über MySQL realisieren. Doch da ich Lua jetzt nicht so wirklich beherrsche, habe ich recherchiert, wie man andere Scripte (z.B. Bash oder eben PHP) ausführen kann.

So wird nun in Lua nun die Basic-Auth Implementierung vorgenommen und der Abgleich mit der Datenbank erledigt ein PHP-Script im Hintergrund.
/etc/nginx/auth/auth.lua:

-- Basic-Auth with lua function authenticate() -- does the auth header exist? local header = ngx.req.get_headers()['Authorization'] if header == nil or header:find(' ') == nil then return false end -- check whether its a Basic-Auth local divider = header:find(' ') if header:sub(0, divider-1) ~= 'Basic' then return false end -- try to decode the Basic-Auth local auth = ngx.decode_base64(header:sub(divider+1)) if auth == nil or auth:find(':') == nil then return false end -- parse the auth information divider = auth:find(':') local username = auth:sub(0, divider-1) local password = auth:sub(divider+1) -- now my lua knowledge is done... -- so request something to validate the information local handle = io.popen(string.format('php /etc/nginx/auth/auth.php %s %s %s', ngx.var.lua_group, username, password)) local response = handle:read("*a") handle:close() -- check the script response if response == nil or response ~= 'OK' then return false end return true end local isValid = authenticate() if not isValid then ngx.header.content_type = 'text/plain' ngx.header.www_authenticate = string.format('Basic realm="%s"', ngx.var.lua_realm) ngx.status = ngx.HTTP_UNAUTHORIZED ngx.say(string.format('ERROR 401: Unauthorized\nArea: %s', ngx.var.lua_realm)) end
/etc/nginx/auth/auth.php
<?php // The MySQL database information $db_host = 'localhost'; $db_port = 3306; $db_name = 'nginx-auth'; $db_user = 'auth-user'; $db_pass = 'some-password'; $group = ''; $user = ''; $password = ''; if (!empty($argv[1])) { $group = trim($argv[1]); } if (!empty($argv[2])) { $user = trim($argv[2]); } if (!empty($argv[3])) { $password = trim($argv[3]); } try { $db = new PDO('mysql:host='.$db_host.';port='.$db_port.';dbname='.$db_name.';', $db_user, $db_pass); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTIION); $query = 'SELECT 1 AS OK FROM `nginx-auth-table` WHERE `is_active` = 1 AND `group` = :group AND `username` = :user AND `password` = SHA2(:pass, 256);'; $stmt = $db->prepare($query); $stmt->bindValue(':group', $group); $stmt->bindValue(':user', $user); $stmt->bindValue(':pass', $password); $stmt->execute(); $res = $stmt->fetchAll(PDO::FETCH_OBJ); if (count($res) == 1) { echo 'OK'; exit(0); } } catch (Exception $ex) { echo 'ERROR: '.$ex->getMessage(); exit(1); } echo 'ERROR: Invalid credentials'; exit(1); ?>

Ein kurzes Fazit

Ich persönlich bin immer wieder sehr erstaunt darüber, wie groß die Palette an Optionen ist, mit der man NginX erweitern kann. Dabei muss man sich zwar manchmal ein paar Tricks überlegen, doch letzten Endes gibt es immer eine Lösung.
Die Idee über Lua ein beliebiges anderes Script auszuführen eröffnet viele neue Möglichkeiten, wie man die Authentifizierung lösen könnte (PHP war lediglich meine Wahl).

Dateien