07 Oktober 2014 Engineering

HTTP-Tricks mit Nginx

Von Karel Minařík

Update November 2, 2015: If you're interested in advanced access control configuration or other security features, consider taking Shield, security for Elasticsearch, for a spin.

Elasticsearch zeichnet sich hauptsächlich dadurch aus, dass es (grob gesagt) als RESTful-Service über HTTP bereitgestellt wird.

Die Vorteile liegen klar auf der Hand: Alle Web-Entwickler sind bereits mit der API vertraut und sie wissen, wie sie funktioniert. Außerdem lässt es sich quasi ohne Hilfsmittel über den cURL-Befehl im Browser verwenden. Es ist außerdem leicht, API-Wrapper in verschiedenen Programmiersprachen zu schreiben.

Doch es gibt noch tiefgreifendere Gründe, weshalb Elasticsearch einen HTTP-basierten Ansatz verfolgt: Dadurch fügt es sich nämlich hervorragend in das vorhandene System der Software-Entwicklung und -Architektur ein.

HTTP ist ein architektonisches System

Das typische moderne Software- oder Informationssystem ist ziemlich oft eine Sammlung von locker verknüpften Services, die über ein Netzwerk kommunizieren: meistens via HTTP. Aus Designersicht ist ein wichtiger Aspekt dieser Strategie, dass man die Servicekette immer „auseinanderreißen“ und ein anderes Element in den „Stack“ einfügen kann, das eine neue Funktionalität hinzufügt oder diese ändert. Früher kannte man solche Programme unter der Bezeichnung „Middleware“, doch sie sind im Kontext von RESTful-Web-Services wieder aus der Versenkung aufgetaucht, zum Beispiel als Rack-Middleware, die insbesondere im Framework Ruby on Rails eingesetzt wird.

HTTP ist für solche Architekturen besonders gut geeignet, denn die vermeintlichen Mängel (fehlender Status, textbasierte Repräsentation, URI-zentrische Semantik usw.) stellen sich schnell als Vorteil heraus: „Middleware“ muss nicht zu einem bestimmten Element in der „Kette“ passen, sondern übermittelt einfach nur Statuscodes, Header und Haupttexte. In diesem Sinne ist HTTP praktisch transparent. Es macht zum Beispiel keinen Unterschied, ob Sie ein Bild vom Original-Webserver abrufen oder auf einem anderen Kontinent zwischenspeichern. Es bleibt in beiden Fällen ein und dieselbe „Ressource“.

Caching, bzw. das Zwischenspeichern, ist ein Paradebeispiel für diesen Aspekt von HTTP, das bereits in Roy Fieldings Seminararbeit über RESTful-Architekturen behandelt wurde. (Weitere Informationen zu diesem Thema finden Sie in Ryan Tomaykos Things Caches Do und in Mark Nottinghams Caching Tutorial.)

Technisch gesehen übernimmt der Zwischenspeicher hier die Funktion eines Proxys Er „steht für“ ein anderes Element im Stack.

Doch Proxys können noch so viel mehr. Ein gutes Beispiel ist Authentifizierung und Autorisierung: ein Proxy kann Anfragen an einen Service abfangen und Authentifizierungs- sowie Autorisierungsaufgaben übernehmen und dann den Zugriff für den Client erlauben oder ablehnen.

Diese Art von Proxy nennt man üblicherweise Reverse Proxy (umgekehrter Proxy). Der Name ergibt dahingehend Sinn, dass ein traditioneller Proxy Traffic aus einem lokalen Remote-Netzwerk (dem Internet) „weiterleitet“, was in diesem Fall umgekehrt abläuft, denn der Reverse Proxy leitet Anfragen aus dem Internet an ein „lokales“ Backend weiter. Solch ein Proxy kann in einer Programmiersprache wie Java oder Go oder mit einem Framework wie Node.js implementiert werden. Alternativ können wir einen konfigurierbaren Webserver wie Nginx verwenden.

Nginx

Nginx ist ein Open-Source-Webserver, der ursprünglich von Igor Sysoev entwickelt wurde. Er konzentrierte sich dabei auf hohe Performance, parallele Verarbeitung und nur wenig Speicherplatzbedarf. (Einen detaillierten technischen Überblick finden Sie im entsprechenden Kapitel des Buches The Architecture of Open Source Applications.)

Nginx wurde von Anfang an in Hinblick auf seine Funktion als Proxy entwickelt und unterstützt viele zugehörige Konfigurationsanweisungen und -optionen. Üblicherweise wird Nginx als Load Balancer vor Ruby on Rails bei Django-Anwendungen eingesetzt. Viele große PHP-Anwendungen setzen Nginx sogar vor Apache runningmod_php, um die Bereitstellung statischer Inhalte zu beschleunigen und die Anwendung zu skalieren. Dieser Artikel geht hauptsächlich von Nginx als Standardinstallationaus, doch die detaillierteren Abschnitte beschäftigen sich auch mit dem Lua-Modul für Nginx.

Um Nginx als „100-prozentig transparenten“ Proxy für Elasticsearch auszuführen, müssen wir nur minimal etwas an der Konfiguration ändern:

http {
  server {
    listen 8080;
    location / {
      proxy_pass http://localhost:9200;
    }
  }
}

Wenn wir eine Anfrage an http://localhost:8080 stellen, erhalten wir eine Antwort von Elasticsearch, das auf dem Port 9200 läuft.

Dieser Proxy ist natürlich ziemlich nutzlos; er übergibt nur die Daten vom Client an Elasticsearch. Doch dem aufmerksamen Leser ist vielleicht schon aufgefallen, dass dieser durchaus etwas zum „Stack“ hinzufügt: und zwar das Logging jeder Anfrage.

Anwendungsfälle

In diesem Artikel behandeln wir einige interessante Anwendungsfälle für Nginx als Reverse Proxy für Elasticsearch.

Bestehende HTTP-Verbindungen

Beginnen wir mit einem ganz simplen Beispiel: Wir verwenden Nginx als Proxy, der die Verbindung zu Elasticsearch aufrecht erhält. Was haben wir davon? Hauptsächlich möchten wir Elasticsearch die Last abnehmen, eine Verbindung für jede Anfrage immer wieder öffnen und schließen zu müssen, was bei der Verwendung eines Clients ohne Support für bestehende Verbindungen der Fall wäre. Elasticsearch hat neben den Netzwerkfunktionen viele weitere Aufgaben und das Öffnen/Schließen von Verbindungen kostet wertvolle Zeit und Ressourcen (zum Beispiel durch das Limit an maximal geöffneten Dateien).

Die komplette Konfiguration finden Sie, wie übrigens alle Beispiele aus diesem Artikel, hier.

events {
    worker_connections  1024;
}
http {
  upstream elasticsearch {
    server 127.0.0.1:9200;
    keepalive 15;
  }
  server {
    listen 8080;
    location / {
      proxy_pass http://elasticsearch;
      proxy_http_version 1.1;
      proxy_set_header Connection "Keep-Alive";
      proxy_set_header Proxy-Connection "Keep-Alive";
    }
  }
}

Starten wir Nginx mit dieser Konfiguration:

$ nginx -p $PWD/nginx/ -c $PWD/nginx_keep_alive.conf

Wenn Sie eine Anfrage direkt an Elasticsearch stellen, fällt Ihnen mit Sicherheit auf, dass die Anzahl an geöffneten Verbindungen immer weiter zunimmt:

$ curl 'localhost:9200/_nodes/stats/http?pretty' | grep total_opened
# "total_opened" : 13
$ curl 'localhost:9200/_nodes/stats/http?pretty' | grep total_opened
# "total_opened" : 14
# ...

Doch beim Einsatz von Nginx sieht das völlig anders aus – die Anzahl an geöffneten Verbindungen bleibt gleich:

$ curl 'localhost:8080/_nodes/stats/http?pretty' | grep total_opened
# "total_opened" : 15
$ curl 'localhost:9200/_nodes/stats/http?pretty' | grep total_opened
# "total_opened" : 15
# ...

Einfacher Load Balancer

Mit lediglich einer kleinen Konfigurationsänderung können wir damit Anfragen an mehrere Elasticsearch-Nodes weiterreichen, wobei wir Nginx als leichten Load Balancer verwenden:

events {
    worker_connections  1024;
}
http {
  upstream elasticsearch {
    server 127.0.0.1:9200;
    server 127.0.0.1:9201;
    server 127.0.0.1:9202;
    keepalive 15;
  }
  server {
    listen 8080;
    location / {
      proxy_pass http://elasticsearch;
      proxy_http_version 1.1;
      proxy_set_header Connection "Keep-Alive";
      proxy_set_header Proxy-Connection "Keep-Alive";
    }
  }
}

Wie Sie sehen, haben wir zwei zusätzliche Nodes in der Upstream-Anweisung hinzugefügt. Nginx verteilt die Anfragen nun automatisch im Rundlauf-Verfahren an diese Server und teilt die Last des Elasticsearch-Clusters gleichmäßig auf alle Nodes auf:

$ curl localhost:8080 | grep name
# "name" : "Silver Fox",
$ curl localhost:8080 | grep name
# "name" : "G-Force",
$ curl localhost:8080 | grep name
# "name" : "Helleyes",
$ curl localhost:8080 | grep name
# "name" : "Silver Fox",
# ...

Dieses Verhalten ist gewünscht, denn es verhindert, dass ein einziger Node „heiß läuft“, also ständig aktiv ist, da er immer wieder passiert wird, weil er die regulären Aufgaben eines Nodes erfüllen und außerdem noch den ganzen Traffic weiterleiten und alle weiteren ihm zugewiesenen Aktionen durchführen muss. Um diese Konfiguration zu ändern, müssen wir einfach nur in der Upstream-Anweisung die Serverliste aktualisieren und Nginx ein Signal zum Neuladen schicken.

Weitere Informationen über die Load Balancing-Funktionen von Nginx, wie verschiedene Balancing-Strategien, die „Gewichtung“ für verschiedene Nodes, Status-Checks und Live-Monitoring, finden Sie im Artikel Load Balancing with NGINX and NGINX Plus.

(Bitte beachten Sie, dass die offiziellen Elasticsearch-Clients solche Load Balancing-Aktionen selbst durchführen können, indem sie Node-Listen im Cluster automatisch neu laden und die Anfrage erneut an einen anderen Node stellen usw.)

Einfache Authentifizierung

Schauen wir uns eine weitere Funktionalität an: Authentifizierung und Autorisierung. Standardmäßig verhindert Elasticsearch unautorisierte Zugriffe nicht, denn es wurde nicht als offener Service entwickelt. Wenn Sie allerdings den offenen Zugriff auf Port 9200 erlauben, sind Sie für Datendiebstahl und -verlust anfällig und es kann sogar das gesamte System gefährdet sein.

Normalerweise können Sie Ihr Elasticsearch-Cluster schützen, indem Sie den Zugriff über VPN, Firewall-Regeln, AWS-Sicherheitsgruppen usw. einschränken. Was passiert jedoch, wenn Sie sich von außen mit einem Cluster verbinden möchten oder müssen, und die Authentifizierung mit einem Benutzernamen und einem Passwort erforderlich ist?

Erinnern wir uns an das oben vorgestellte Proxy-Konzept. Das könnte doch funktionieren? Wir müssen die Anfragen an Elasticsearch nur unterbrechen, den Client autorisieren und den Zugriff erlauben oder verweigern. Da Nginx die einfache Zugriffsauthentifizierung standardmäßig unterstützt, ist es auch absolut unkompliziert:

events {
  worker_connections  1024;
}
http {
  upstream elasticsearch {
    server 127.0.0.1:9200;
  }
  server {
    listen 8080;
    auth_basic "Protected Elasticsearch";
    auth_basic_user_file passwords;
    location / {
      proxy_pass http://elasticsearch;
      proxy_redirect off;
    }
  }
}

Wir können die Passwortdatei mit vielen Dienstprogrammenerstellen, zum Beispiel OpenSSL:

$ printf "john:$(openssl passwd -crypt s3cr3t)n" > passwords

Führen wir Nginx nun mit dieser Konfiguration aus (denken Sie daran, den Nginx-Prozess vorher zu schließen):

$ nginx -p $PWD/nginx/ -c $PWD/nginx_http_auth_basic.conf

Wenn wir versuchen, ohne richtige Zugangsdaten auf den Proxy zuzugreifen, wird die Anfrage abgelehnt:

$ curl -i localhost:8080
# HTTP/1.1 401 Unauthorized
# ...

Doch mit den richtigen Zugangsdaten wird der Zugriff genehmigt:

$ curl -i john:s3cr3t@localhost:8080
# HTTP/1.1 200 OK
# ...

Wir können nun den Zugriff auf den Port 9200 für das lokale Netzwerk (z. B. mit Firewall-Regeln) einschränken und nur den Port 8080 für den Zugriff von außen offen lassen. Jeder Client, der auf Elasticsearch zugreifen möchte, muss die richtigen Zugangsdaten kennen.

Einfache Autorisierung

Eine sichere Methode, um von außen Zugriff auf das Cluster zu erhalten, ist natürlich toll, doch Ihnen ist bestimmt schon aufgefallen, dass die Autorisierung nicht einschränkbar ist. Nachdem der Zugriff einmal erlaubt wurde, kann der Client im Cluster machen, was er will: Daten ändern oder löschen, interne Statistiken auslesen und sogar das Cluster abschalten.

Eine einfache Möglichkeit zur Autorisierung des Zugriffs wäre die Ablehnung von Anfragen für bestimmte Endpunkte, damit nur ein Client, der auf einer lokalen Maschine oder in einem Netzwerk läuft, darauf zugreifen darf. Wir können die Ortsanweisungen ein bisschen anpassen:

location / {
  if ($request_filename ~ _shutdown) {
    return 403;
    break;
  }
  proxy_pass http://elasticsearch;
  proxy_redirect off;
}

Schließen wir Nginx und starten es mit der neuen Konfiguration:

$ nginx -p $PWD/nginx/ -c $PWD/nginx_http_auth_deny_path.conf

Wenn wir jetzt eine Anfrage an die abgeschaltete API stellen, wird diese abgelehnt (obwohl wir die richtigen Zugangsdaten haben):

$ curl -i -X POST john:s3cr3t@localhost:8080/_cluster/nodes/_shutdown
# HTTP/1.1 403 Forbidden
# ....

Wir können es auch andersherum versuchen, indem wir nur bestimmte Endpunkte erlauben, wie zum Beispiel administrative APIs. Den Zugriff auf alles andere verweigern wir. Wir unterscheiden dabei zwischen zwei separaten Ortsanweisungen:

events {
  worker_connections  1024;
}
http {
  upstream elasticsearch {
    server 127.0.0.1:9200;
  }
  server {
    listen 8080;
    auth_basic "Protected Elasticsearch";
    auth_basic_user_file passwords;
    location ~* ^(/_cluster|/_nodes) {
      proxy_pass http://elasticsearch;
      proxy_redirect off;
    }
    location / {
      return 403;
      break;
    }
  }
}

Authentifizierte Anfragen an /_clsuter und /_nodes APIs sind erlaubt, aber alles andere wird abgelehnt:

$ curl -i john:s3cr3t@localhost:8080/
HTTP/1.1 403 Forbidden
# ...
$ curl -i john:s3cr3t@localhost:8080/_cluster/health
# HTTP/1.1 200 OK
# ...
$ curl -i john:s3cr3t@localhost:8080/_nodes/stats
HTTP/1.1 200 OK
# ...

Selektive Autorisierung

Werfen wir einen Blick auf einen anderen Anwendungsfall für die Autorisierung: Wir möchten das Elasticsearch-Cluster mit einer einfachen Authentifizierung schützen, aber eine HEAD-Anfrage an / erlauben – in den Client-Bibliotheken spricht man vom sogenannten „Pingen“, was zum Beispiel zu Monitoring-Zwecken eingesetzt wird.

Das klingt vielleicht ganz leicht, aber ganz so einfach ist das in der Nginx-Konfiguration nicht: Wir müssen zwei Bedingungen für solch eine Regel festlegen (Anfrage-URL und Methode) und die IF-Anweisung von Nginx erlaubt das nicht. (Nginx betrachtet die IF-Anweisung sogar als „bösartig“.)

Was machen wir in diesem Fall? So wie es aussieht, können wir zwei zentrale Bestandteile der Konfigurationssyntax von Nginx flexibel verwenden: Variablen und benutzerdefinierte Fehlercodes:

events {
  worker_connections  1024;
}
http {
  upstream elasticsearch {
    server 127.0.0.1:9200;
  }
  server {
    listen 8080;
    location / {
      error_page 590 = @elasticsearch;
      error_page 595 = @protected_elasticsearch;
      set $ok 0;
      if ($request_uri ~ ^/$) {
        set $ok "${ok}1";
      }
      if ($request_method = HEAD) {
        set $ok "${ok}2";
      }
      if ($ok = 012) {
        return 590;
      }
      return 595;
    }
    location @elasticsearch {
      proxy_pass http://elasticsearch;
      proxy_redirect off;
    }
    location @protected_elasticsearch {
      auth_basic           "Protected Elasticsearch";
      auth_basic_user_file passwords;
      proxy_pass http://elasticsearch;
      proxy_redirect off;
    }
  }
}

Zuerst definieren wir zwei benutzerdefinierte Statusfehler-Codes: 590 (für den Zugriff auf Elasticsearch ohne Zugangsdaten) und 595 (für den Zugriff auf Elasticsearch mit einer einfachen Authentifizierung, wie wir es bisher gemacht haben). Wir verwenden die Funktion „benannte Orte“ von Nginx, um diese beiden Codes zu unterscheiden. Beide verweisen auf dasselbe Cluster, aber einer benötigt eine Authentifizierung.

Dann legen wir eine Variable $ok fest, die den Standardwert 0 hat. Wenn die eingehende Anfrage-URL / entspricht (d. h. der Pfad ist leer), hängen wir 1 an. Wenn diese Anfrage auch über die HEAD-Methode durchgeführt wird, hängen wir 2 an. Wenn beide Bedingungen erfüllt sind, ist der resultierende Wert von $ok gleich 012.

Und genau das prüfen wir bei der letzten IF-Anweisung. In diesem Fall geben wir den Statuscode 590 zurück, bzw. wir erlauben der Anfrage den Zugriff auf Elasticsearch. In jedem anderen Fall benötigen wir eine Authentifizierung:

$ curl -i -X HEAD localhost:8080
# HTTP/1.1 200 OK
# ...
$ curl -i localhost:8080
# HTTP/1.1 401 Unauthorized
# ...
$ curl -i john:s3cr3t@localhost:8080
# HTTP/1.1 200 OK
# ...

Mehrere Rollen für die Autorisierung

Bisher war unser Autorisierungsschema noch recht einfach. Doch was passiert, wenn wir ein komplexeres Schema benötigen, das auf Rollen basiert? So etwas wie:

  • nicht authentifizierte Clients können nur auf die „Ping“-URL zugreifen (HEAD /),
  • ein Client, der sich mit den Benutzer-Zugangsdaten autorisiert hat, kann Anfragen wie perform_search und analyserequests durchführen,
  • ein Client, der sich mit den Admin-Zugangsdaten authentifiziert hat, kann jede Anfrage durchführen.

Hier gehen wir ganz anders vor. Wir erstellen einen virtuellen Server für jede Rolle:

events {
  worker_connections  1024;
}
http {
  upstream elasticsearch {
      server 127.0.0.1:9200;
  }
  # Allow HEAD / for all
  #
  server {
      listen 8080;
      location / {
        return 401;
      }
      location = / {
        if ($request_method !~ "HEAD") {
          return 403;
          break;
        }
        proxy_pass http://elasticsearch;
        proxy_redirect off;
      }
  }
  # Allow access to /_search and /_analyze for authenticated "users"
  #
  server {
      listen 8081;
      auth_basic           "Elasticsearch Users";
      auth_basic_user_file users;
      location / {
        return 403;
      }
      location ~* ^(/_search|/_analyze) {
        proxy_pass http://elasticsearch;
        proxy_redirect off;
      }
  }
  # Allow access to anything for authenticated "admins"
  #
  server {
      listen 8082;
      auth_basic           "Elasticsearch Admins";
      auth_basic_user_file admins;
      location / {
        proxy_pass http://elasticsearch;
        proxy_redirect off;
      }
  }
}

Wir generieren die Zugangsdaten erneut mit dem OpenSSL-Befehl:

$ printf "user:$(openssl passwd -crypt user)n"   > users
$ printf "admin:$(openssl passwd -crypt admin)n" > admins

Jetzt kann jeder das Cluster „anpingen“, aber nichts weiter:

$ curl -i -X HEAD localhost:8080
# HTTP/1.1 200 OK
$ curl -i -X GET localhost:8080
# HTTP/1.1 403 Forbidden

Authentifizierte Benutzer haben Zugriff auf die Such- und Analyse-APIs, aber auf keine weiteren APIs:

$ curl -i localhost:8081/_search
# HTTP/1.1 401 Unauthorized
# ...
$ curl -i user:user@localhost:8081/_search
# HTTP/1.1 200 OK
# ...
$ curl -i user:user@localhost:8081/_analyze?text=Test
# HTTP/1.1 200 OK
# ...
$ curl -i user:user@localhost:8081/_cluster/health
# HTTP/1.1 403 Forbidden
# ...

Authentifizierte Admins haben natürlich auf alle APIs Zugriff:

$ curl -i admin:admin@localhost:8082/_search
# HTTP/1.1 200 OK
# ...
$ curl -i admin:admin@localhost:8082/_cluster/health
# HTTP/1.1 200 OK
# ...

Ihnen ist vielleicht schon aufgefallen, dass jede Rolle über einen anderen Port auf den Proxy zugreift: das ist der Preis, den wir für diese Lösung zahlen müssen. Andererseits sollte es recht einfach sein, eine beliebige Anwendung für ein Schema dieser Art zu konfigurieren, zum Beispiel für die Verwendung unterschiedlicher Clients, die mit verschiedenen URLs verknüpft sind. (Wir hätten stattdessen auch die Anweisung server_name nutzen können, um die einzelnen Server unterscheiden zu können, um dann alle Server stattdessen über denselben Port laufen zu lassen).

Zugriffskontrollliste mit Lua

Bisher konnten wir einigermaßen komplexe, Nicht-Hallo-Welt-Szenarien mit Nginx als Proxy für Elasticsearch unterstützen. Allerdings ist sogar das letzte Beispiel ziemlich einfach und die Unterstützung eines komplexeren, detaillierteren Autorisierungsschemas wäre ziemlich umständlich – denken Sie nur an die ganzen möglichen Server-Sperren ...

Was passiert also, wenn wir eine komplexere Reihe an Regeln unterstützen müssen, zum Beispiel, dass wir nicht nur konkrete Endpunkte für bestimmte Rollen erlauben, sondern nur bestimmte Methoden, und dass wir die Informationen in einem gängigeren Format speichern möchten?

In der nächsten Konfiguration verwenden wir das Lua-Modul für Nginx, um die Regeln und den Code expliziter ausdrücken zu können. Wir werden das vom OpenResty-Projekt bereitgestellte Paket verwenden, was nicht nur Lua-, sondern auch JSON-Parser, eine Redis-Bibliothek und viele andere Lua-Module mit Nginx enthält. Bitte schauen Sie sich die Installationsanweisungen auf der Website von OpenResty an. Auf einem Mac können Sie die Homebrew-Formel verwenden:

$ brew install https://raw.githubusercontent.com/Homebrew/homebrew-nginx/master/openresty.rb

Das OpenResty-Paket verwandelt Nginx in einen vollständigen Web-Anwendungsserver, der es erlaubt, Orte mit Lua-Code neu zu schreiben, wobei unter anderem externe Datenbanken verwendet, Antworten spontan manipuliert und HTTP-Unteranfragengestellt werden. In unserem Beispiel verwenden wir die Anweisung access_by_lua_file in Übereinstimmung mit der regulären einfachen HTTP-Authentifizierung, um den Client zu genehmigen oder abzulehnen.

Die Nginx-Konfiguration selbst ist recht einfach:

error_log logs/lua.log notice;
events {
  worker_connections 1024;
}
http {
  upstream elasticsearch {
    server 127.0.0.1:9200;
  }
  server {
    listen 8080;
    location / {
      auth_basic           "Protected Elasticsearch";
      auth_basic_user_file passwords;
      access_by_lua_file '../authorize.lua';
      proxy_pass http://elasticsearch;
      proxy_redirect off;
    }
  }
}

Der Lua-Code ist natürlich ein bisschen komplizierter. In der Online-Version finden Sie alle Informationen mit Kommentaren, Fehlerprotokollierung usw.:

-- authorization rules
local restrictions = {
  all  = {
    ["^/$"]                             = { "HEAD" }
  },
  user = {
    ["^/$"]                             = { "GET" },
    ["^/?[^/]*/?[^/]*/_search"]         = { "GET", "POST" },
    ["^/?[^/]*/?[^/]*/_msearch"]        = { "GET", "POST" },
    ["^/?[^/]*/?[^/]*/_validate/query"] = { "GET", "POST" },
    ["/_aliases"]                       = { "GET" },
    ["/_cluster.*"]                     = { "GET" }
  },
  admin = {
    ["^/?[^/]*/?[^/]*/_bulk"]          = { "GET", "POST" },
    ["^/?[^/]*/?[^/]*/_refresh"]       = { "GET", "POST" },
    ["^/?[^/]*/?[^/]*/?[^/]*/_create"] = { "GET", "POST" },
    ["^/?[^/]*/?[^/]*/?[^/]*/_update"] = { "GET", "POST" },
    ["^/?[^/]*/?[^/]*/?.*"]            = { "GET", "POST", "PUT", "DELETE" },
    ["^/?[^/]*/?[^/]*$"]               = { "GET", "POST", "PUT", "DELETE" },
    ["/_aliases"]                      = { "GET", "POST" }
  }
}
-- get authenticated user as role
local role = ngx.var.remote_user
-- exit 403 when no matching role has been found
if restrictions[role] == nil then
  ngx.header.content_type = 'text/plain'
  ngx.status = 403
  ngx.say("403 Forbidden: You don't have access to this resource.")
  return ngx.exit(403)
end
-- get URL
local uri = ngx.var.uri
-- get method
local method = ngx.req.get_method()
local allowed  = false
for path, methods in pairs(restrictions[role]) do
  -- path matched rules?
  local p = string.match(uri, path)
  local m = nil
  -- method matched rules?
  for _, _method in pairs(methods) do
    m = m and m or string.match(method, _method)
  end
  if p and m then
    allowed = true
  end
end
if not allowed then
  ngx.header.content_type = 'text/plain'
  ngx.log(ngx.WARN, "Role ["..role.."] not allowed to access the resource ["..method.." "..uri.."]")
  ngx.status = 403
  ngx.say("403 Forbidden: You don't have access to this resource.")
  return ngx.exit(403)
end

Wie Sie sehen, speichern wir die Rollenliste als Lua-Tabelle mit einer verschachtelten Tabelle für jede Rolle. Eine eingehende Anfragemethode und eine URL werden mit regulären Ausdrucksmustern abgeglichen und wenn sie zueinander passen, wird die Anfrage genehmigt:

$ curl -i -X HEAD 'http://localhost:8080'
# HTTP/1.1 401 Unauthorized
# ...
$ curl -i -X HEAD 'http://all:all@localhost:8080'
# HTTP/1.1 200 OK
# ...
$ curl -i -X GET 'http://all:all@localhost:8080'
# HTTP/1.1 403 Forbidden
# ...
$ curl -i -X GET 'http://user:user@localhost:8080'
# HTTP/1.1 200 OK
# ...
$ curl -i -X POST 'http://user:user@localhost:8080/myindex/mytype/1' -d '{"title" : "Test"}'
# HTTP/1.1 403 Forbidden
# ...
$ curl -i -X DELETE 'http://user:user@localhost:8080/myindex/'
# HTTP/1.1 403 Forbidden
# ...
$ curl -i -X POST 'http://admin:admin@localhost:8080/myindex/mytype/1' -d '{"title" : "Test"}'
# HTTP/1.1 200 OK
# ...
$ curl -i -X DELETE 'http://admin:admin@localhost:8080/myindex/'
# HTTP/1.1 200 OK
# ...

Natürlich könnte die Restriktionstabelle noch viel komplizierter sein, die Authentifizierung könnte von einem Oauth-Token anstelle einer einfachen HTTP-Authentifizierung bereitgestellt werden usw., doch die Autorisierungsmechanismen würden sich nicht ändern.

Schlussfolgerung

In diesem Artikel haben wir es uns zunutze gemacht, dass Elasticsearch ein Service ist, der über HTTP bereitgestellt wird. Wir haben eine Reihe an Funktionen zu Elasticsearch hinzugefügt, ohne die Software selbst in irgendeiner Art und Weise zu erweitern oder zu ändern.

Wir haben gesehen, dass HTTP als unabhängiger, entkoppelter Service sehr gut konzeptuell in das aktuelle architektonische System von Designsoftware passt, und wir wissen, dass Nginx als individuell anpassbarer Hochleistungs-Proxy verwendet werden kann. Viel Spaß beim „Proxying“!