pw
Veröffentlicht: Aktualisiert: Autor: Kategorie: Cybersecurity Lesezeit: 5 min ID: 378

Wie Du endlich richtige Logs erzeugst: NGINX Logs als JSON

Von unlesbaren Textzeilen zu maschinenlesbaren Ereignissen

Die meisten Server schreiben ihre Logs bis heute in einem Format, das aus den Neunzigern stammt. Eine Zeile pro Anfrage, Werte durch Leerzeichen getrennt, halb in Anführungszeichen, halb nicht. Für einen schnellen Blick reicht das. Sobald du aber wirklich etwas aus deinen Logs herausholen willst, wird es zur Qual. Die Lösung ist unspektakulär und ändert trotzdem alles: Schreib deine Logs als JSON.

Das Problem mit den Standard-Logs

Das Standardformat von NGINX sieht harmlos aus und ist ein Albtraum, sobald du es auswerten willst. Ein Zeitstempel hier, eine IP da, der User-Agent irgendwo in Anführungszeichen am Ende, dazwischen Statuscode und Größe. Willst du daraus die langsamsten Anfragen der letzten Stunde ziehen, fängst du an, reguläre Ausdrücke zu bauen. Und jedes Mal, wenn ein Wert ein Leerzeichen oder ein Anführungszeichen enthält, das da nicht hingehört, bricht dein Muster.

Das ist kein Werkzeugproblem, das ist ein Formatproblem. Unstrukturierter Text zwingt jeden, der ihn liest, das Format zu erraten. Ein Mensch kann das. Eine Maschine hasst es.

Warum JSON

JSON dreht das um. Jede Logzeile wird ein sauberes Objekt mit benannten Feldern. Kein Raten mehr, welche Position welcher Wert ist, sondern klare Schlüssel: Methode, Status, Dauer, Header. Ein Programm liest das direkt ein, ohne einen einzigen regulären Ausdruck.

Der Gewinn ist sofort spürbar. Du kannst deine Logs nach Feldern filtern, sortieren und zählen. Du kannst sie ohne Umbau in Werkzeuge wie jq, Loki, Elasticsearch oder OpenSearch kippen. Und du kannst neue Felder hinzufügen, ohne dass ältere Auswertungen kaputtgehen, weil jedes Feld seinen Namen trägt und nicht seine Position.

Die Konfiguration

NGINX bringt alles mit, was du brauchst. Der Schlüssel ist die Option escape=json, die dafür sorgt, dass Sonderzeichen in den Werten korrekt maskiert werden, sodass immer gültiges JSON entsteht. Das folgende Format landet in der http-Sektion deiner NGINX-Konfiguration:

http {
    log_format custom_json escape=json '{'
        '"level":"info",'
        '"ts":"$time_iso8601",'
        '"message":"handled request $request_method $request_uri",'
        '"request":{'
            '"id":"$http_x_request_id",'
            '"remote_ip":"$remote_addr",'
            '"remote_port":"$remote_port",'
            '"protocol":"$server_protocol",'
            '"method":"$request_method",'
            '"host":"$host",'
            '"uri":"$request_uri",'
            '"referer":"$http_referer",'
            '"headers":{'
                '"user_agent":"$http_user_agent",'
                '"accept":"$http_accept",'
                '"accept_encoding":"$http_accept_encoding",'
                '"accept_language":"$http_accept_language"'
            '}'
        '},'
        '"response":{'
            '"status":$status,'
            '"body_bytes_sent":$body_bytes_sent,'
            '"request_length":$request_length,'
            '"request_time":$request_time,'
            '"upstream_response_time":"$upstream_response_time"'
        '}'
    '}';
}

Was in den Feldern steckt

Das ist kein zufälliger Satz an Feldern, jedes hat einen Zweck.

Die id aus dem Header X-Request-ID erlaubt dir, eine einzelne Anfrage über mehrere Systeme hinweg zu verfolgen, vom NGINX über die Anwendung bis zur Datenbank. Ohne diese Klammer suchst du in jedem System einzeln. Mit ihr ziehst du eine Anfrage als Ganzes zusammen.

Die request_time misst, wie lange die Anfrage insgesamt gedauert hat, die upstream_response_time, wie lange dein Backend gebraucht hat. Der Unterschied zwischen beiden verrät dir, wo die Zeit verloren geht.

Und dann die Header. user_agent, accept, accept_encoding, accept_language. Wer die Serie hier auf pw.is verfolgt, erkennt sie sofort wieder. Genau diese Felder sind das Rohmaterial für die Interessenbestimmung, also die Frage, ob hinter einer Anfrage ein echter Besucher steckt oder ein Automat. In sauberem JSON lässt sich das endlich in Ruhe auswerten, statt es mühsam aus einer Textzeile zu klauben.

Ein praktischer Hinweis noch: Jeder beliebige Request-Header steht in NGINX als Variable $http_<name> zur Verfügung, kleingeschrieben und mit Unterstrichen. Aus dem Header X-Request-ID wird $http_x_request_id, aus einem eigenen CSRF-Header $http_csrf. Du kannst dein Log also genau um die Felder erweitern, die für dich zählen.

Aktivieren

Das Format allein tut nichts, du musst es einer Anfrage-Verarbeitung zuweisen. Das passiert mit der access_log-Direktive, entweder global im http-Block oder gezielt pro server:

access_log /var/log/nginx/access.log custom_json;

Danach prüfst du die Konfiguration und lädst NGINX neu, ohne die Verbindungen zu unterbrechen:

nginx -t && systemctl reload nginx

Ab jetzt schreibt jede Anfrage eine Zeile sauberes JSON.

Was du damit machst

Jetzt kommt der Teil, für den sich der Aufwand lohnt. Mit einem JSON-Log kannst du direkt auf der Kommandozeile arbeiten, ohne ein einziges Analyse-Werkzeug zu installieren. Ein Beispiel mit jq, das dir die zehn langsamsten Anfragen zeigt:

tail -n 10000 /var/log/nginx/access.log \
  | jq -r 'select(.response.request_time|tonumber > 1)
           | "\(.response.request_time)  \(.request.method) \(.request.uri)"' \
  | sort -rn | head

Dasselbe Log wandert ohne Umbau in eine zentrale Sammlung wie Loki oder Elasticsearch, wo du es durchsuchst, in Dashboards legst und Alarme darauf baust. Und weil jedes Feld benannt ist, kannst du Muster erkennen, die im Textlog untergehen: viele Anfragen mit fehlendem accept-Header, ungewöhnlich schnelle Zugriffe, verdächtige Statuscodes in Folge. Das ist die Grundlage für alles, was ich in dieser Serie über das Lesen von Signalen geschrieben habe. Erst wenn die Daten strukturiert vorliegen, kannst du sie wirklich befragen.

Datenschutz nicht vergessen

Ein Log wie dieses enthält personenbezogene Daten, allen voran die IP-Adresse. Damit gilt die Datenschutz-Grundverordnung, und ein paar Dinge gehören bedacht, bevor du das produktiv laufen lässt.

Lege eine Speicherfrist fest und halte sie mit einer Log-Rotation ein, statt Logs unbegrenzt zu horten. Prüfe, ob du die IP-Adresse für deinen Zweck wirklich vollständig brauchst, oder ob eine Kürzung des letzten Adressteils reicht. Und halte in deinem Verarbeitungsverzeichnis fest, warum du diese Daten erhebst. Für die Angriffserkennung ist das ein berechtigtes Interesse, aber der Zweck muss benannt und die Speicherung begrenzt sein. Sauberes Logging heißt nicht, alles für immer zu sammeln, sondern das Richtige gezielt und nachvollziehbar.

Zum Mitnehmen

Der Wechsel von Text zu JSON ist eine Kleinigkeit in der Konfiguration und ein großer Unterschied in der Praxis. Aus Zeilen, die du mühsam auseinandernehmen musst, werden Ereignisse, die du direkt befragen kannst. Wer seine Logs ernst nimmt, weil sie das Gedächtnis seines Systems sind, fängt genau hier an. Und wer sie so aufbereitet, hat die Grundlage gelegt, um Angriffe überhaupt erkennen zu können, bevor sie zum Vorfall werden.