Graylog

Die unterstützten Releases sowie deren End-of-Life-Datum (EOL) findet man auf https://go2docs.graylog.org/current/setting_up_graylog/supported_versions.htm. In der Regel werden die letzten drei dot-Releases unterstützt; das kann sich je nach Versionsnummer mit Major Releases überschneiden. Etwa alle 4 bis 6 Monate erscheint ein neues dot-Release.

Es gibt keine Releases, die als besonders stabil oder instabil gelten. Graylog führt daher auch keine LTS-Versionen. Vor einer intensiven Fehleranalyse wird empfohlen, auf eine neuere Version zu aktualisieren.

Graylog Cloud für Europa wird von AWS in Frankfurt gehostet.

Begriffe
  • Data Node: seit Graylog 6.0 als Standard empfohlen, ein von Graylog verwaltetes OpenSearch-Cluster. Übernimmt TLS-Erzeugung und Cluster-Bootstrap, sodass man OpenSearch nicht mehr eigenständig administrieren muss.

  • Extractor: Regel auf einem Input, die aus dem message-Feld weitere strukturierte Felder extrahiert (Regex, GROK, Split, Copy, JSON). Wird pro Input definiert und läuft auf jedem eingehenden Event. Gilt mittlerweile als Legacy-Feature; für neue Parsing-Logik stattdessen Pipeline + Rule (siehe unten) verwenden.

  • Index Set: Satz gleichartiger OpenSearch-Indices mit eigener Retention-Policy und Shard-/Replica-Konfiguration. Jeder Stream wird mindestens einem Index Set zugewiesen.

  • Input: Quelle, aus der Graylog Messages liest (Syslog UDP/TCP, Beats, GELF, Raw, AWS CloudWatch, Kafka, …). Jede Input-Instanz lauscht auf einem Port und verarbeitet ein Protokoll.

  • Pipeline + Rule: regelbasierte Message-Verarbeitung (weit mächtiger als Extractors). Mehrere Rules werden in Stages einer Pipeline gruppiert; Streams binden Pipelines ein.

  • Sidecar: Log-Collector-Manager, der auf zu überwachenden Hosts läuft und dort filebeat, winlogbeat, nxlog oder auditbeat mit zentral gepflegter Konfiguration startet. Die Collector-Konfigurationen werden in Graylog verwaltet und an die Sidecars ausgeliefert.

  • Stream: Gruppe von Messages, die durch Regeln (Stream Rules) aus dem gesamten Message-Fluss abgezweigt werden. Streams sind Voraussetzung für Alerts, Pipelines und Dashboard-Widgets.

Features, die laut https://go2docs.graylog.org der Graylog Enterprise Edition vorbehalten sind:

  • Audit Log (für Graylog selbst)

  • Correlation Engine

  • Custom Themes and Notifications

  • Google Inputs, Microsoft Defender for Endpoint Input, CrowdStrike Input

  • Graylog Security inkl. Security Interface, Security Events, Anomaly Detection, Asset Enrichment, Investigations, Sigma Rules und Risk Scores

  • Log Ingestion Limit Notification, Cluster-to-Cluster Forwarder

  • OIDC, On-Premise Okta Authentication

  • Routing und Enrichment: Data Warehouse, Indexer and Processing Failures Index, Restore an Archive, Enterprise Output Framework

  • Search Filters

  • Visualize: Reports

Installation

Bemerkung

Unbedingt die zu installierenden Versionen der abhängigen Software-Pakete beachten: https://go2docs.graylog.org/current/downloading_and_installing_graylog/installing_graylog.html

Graylog kann als Linux-Paket (deb, rpm) oder als Docker-Container (docker-compose, Kubernetes) installiert werden.

Minimum:

  • 2x CPUs

  • 6 GB RAM, besser 8 GB

  • 8 GB Platz auf /

Hier wird die Installation von Graylog 6.0.x beschrieben.

dnf --assumeyes install epel-release
dnf --assumeyes install dnf-plugin-versionlock

MongoDB 7.x (inkl. Pinning):

  • Installation siehe MongoDB

  • Nach der Installation ZWINGEND ausführen: dnf versionlock add mongodb-org

OpenSearch 2.15.x (inkl. Pinning):

  • Installation siehe OpenSearch

  • Nach der Installation ZWINGEND ausführen: dnf versionlock add opensearch

  • OpenSearch für Graylog konfigurieren:

    /etc/opensearch/opensearch.yml
    # for a minimum unsecured running state, single node
    cluster.name: graylog
    node.name: ${HOSTNAME}
    path.data: /var/lib/opensearch
    path.logs: /var/log/opensearch
    discovery.type: single-node
    network.host: 0.0.0.0
    action.auto_create_index: false
    plugins.security.disabled: true
    indices.query.bool.max_clause_count: 32768
    

Die eigentliche Installation der Graylog Community Edition kann beginnen:

rpm --upgrade --verbose --hash \
    https://packages.graylog2.org/repo/packages/graylog-6.0-repository_latest.rpm
dnf --assumeyes install graylog-server

dnf versionlock add graylog-server

Konfiguration - es müssen password_secret und root_password_sha2 generiert und konfiguriert werden.

  • password_secret (Passwort für alle Graylog-Nodes im Cluster): < /dev/urandom tr --delete --complement 'A-Za-z0-9' | head --bytes=96; echo

  • root_password_sha2: echo -n "Enter Password: " && head --lines=1 </dev/stdin | tr --delete '\n' | sha256sum | cut --delimiter=' ' --fields=1

Dann:

/etc/graylog/server/server.conf
password_secret = 3JJgCxO...
root_username = graylog-admin
root_password_sha2 = 0621e81...
http_bind_address = 0.0.0.0:9000
elasticsearch_hosts = https://127.0.0.1:9200
elasticsearch_version_probe_attempts = 10
mongodb_version_probe_attempts = 10

SELinux:

setsebool -P httpd_can_network_connect=on

dnf --assumeyes install policycoreutils-python-utils
semanage port --add --type=mongod_port_t --proto=tcp 27017
systemctl daemon-reload
systemctl enable --now graylog-server.service

tail --follow /var/log/opensearch/opensearch.log

Nach der Installation verfügbare Ports:

  • 9000 (tcp): Graylog-GUI und API-Browser per Swagger UI

  • 9200 (tcp): Elasticsearch/OpenSearch

  • 9300 (tcp): Elasticsearch/OpenSearch Node Communication

  • 27017 (tcp): MongoDB

Port 9000 nach aussen öffnen.

Achtung

Wird Graylog das erste mal gestartet, findet sich der Link zum Login inkl. Initial-Passwort in /var/log/graylog-server/server.log.

Wer von Debian 11 auf Debian 12 aktualisiert, muss dafür sorgen, dass auch das openjdk-17-jre installiert ist, ansonsten kann sich der Graylog-Node nicht mehr am Cluster anmelden.

Konfiguration

Index-Sets, Streams, Extractors
  • Neues Index-Set mit alternativen Retention Times / Retention Policies einrichten: System / Indices / Indices > Default Index Set > Edit; als Default Index Set einrichten.

  • Neuen Stream erstellen: Streams > Create stream, „Remove matches from ‚Default Stream‘“ setzen, neues Index-Set verwenden. Manage Rules > Add stream rule > Type: always match - so erhält der neue Stream alle Nachrichten in das neue Default Index-Set.

  • Extractors lassen sich unter System > Inputs > Extractors anlegen. GROK-Patterns orientieren sich an Oracles Java Pattern Class; ein Java-kompatibler Regex-Tester findet sich unter http://www.regexplanet.com/advanced/java/index.html.

Config Ex- und Import
  • Export: System / Inputs > Content Packs: Create a content pack

  • Import: System / Inputs > Content Packs > Select content packs: Import content pack

LDAP

Eine Gruppen-Synchronisation ist in Graylog Open nicht möglich (Enterprise-Feature). Eine bestimmte LDAP-Gruppe lässt sich aber per Search-Filter einschränken, beispielsweise (&(&(uid={0})(objectClass=inetOrgPerson))(memberOf=cn=MYGROUP,cn=groups,cn=accounts,dc=example,dc=org)) (im Beispiel heisst die Gruppe MYGROUP).

Index Sets und Indizes

Ein Elasticsearch-„Index“ hat einen Namen, z.B. „graylog_0“.

Default Settings des „Default index set“ (seit Graylog 6.0):

  • Rotation strategy: Time-Based Size Optimizing - Graylog rotiert nach Zeit und Index-Grösse innerhalb einer konfigurierbaren Min-/Max-Lifetime, nicht mehr nach einer festen Message-Anzahl.

  • Retention strategy: Delete Index

Die früher voreingestellte zählerbasierte Rotation (Rotation nach Message-Anzahl, Default 20’000’000 Messages je Index, maximal 20 Indices) lässt sich beim Anlegen oder Bearbeiten eines Index Sets weiterhin manuell wählen.

Graylog rotiert die Indices selbst und reicht die Strategie nicht 1:1 an OpenSearch/Elasticsearch durch: Ein periodischer Job im Graylog-Server prüft pro Index Set anhand der OpenSearch-/Elasticsearch-Statistiken (Message-Anzahl, Index-Grösse, Alter), ob die konfigurierte Schwelle überschritten ist, legt bei Bedarf einen neuen Index an und schwenkt den Write-Alias um. Eine Ausnahme bilden Data-Stream-basierte Index Sets (Data Tiering): dort übergibt Graylog eine ISM-Policy an OpenSearch, das den Rollover und das Löschen dann selbst ausführt.

Wer eigene Streams definiert, sollte darauf achten, den Haken „Remove matches from ‚All messages‘ stream“ zu setzen, damit Nachrichten nicht doppelt gespeichert werden.

Theoretisch lassen sich mit diesen curl-Aufrufen nach einer Änderung die Indices neu bauen (in der Praxis funktioniert das oft nicht zuverlässig - auch nach einem einfachen Reboot des Graylog-Servers können die alten Indizes noch vorhanden sein):

# if Elastic is in read-only mode, switch it back to normal
curl --request PUT \
    --header "Content-Type: application/json" \
    --data '{"index.blocks.read_only_allow_delete": null}' \
    http://elastic.example.com:9200/_all/_settings

# start retention job
curl --request POST http://graylog.example.com:9000/api/system/indices/ranges/rebuild

Indizes aus Elasticsearch auslesen:

curl 'http://elastic.example.com:9200/_cat/indices?v'

Indizes-Statistiken:

curl 'http://elastic.example.com:9200/_stats'

Indizes leeren:

curl --request DELETE 'http://elastic.example.com:9200/_all'

Inputs und Nachrichten

Input: Syslog

Soll Graylog Logdaten entgegennehmen, müssen die unter „System > Inputs“ konfiguriert werden.

Für das althergebrachte Syslog Port 1514 (udp) verwenden und lokale Firewall öffnen.

Bemerkung

Graylog kann nicht auf Port 514 hören, das führt zu einem „Permission denied, Failed to bind to: ip-address:514“, da es sich hier um einen privilegierten Port handelt. Daher wie in allen Beispielen Port 1514 verwenden. Um einen Bind auf jede IP-Adresse einzurichten, 0.0.0.0 verwenden, * funktioniert als Angabe nicht.

Nachrichtenformat

Welche Felder sind Standard und/oder Pflicht?

  • message: das einzige Pflichtfeld, muss gesetzt werden; ohne gesetzte „message“ wird Nachricht nicht angenommen.

Felder, welche automatisch gesetzt werden:

  • source (wenn nicht gesetzt, wird die IP des sendenden Hosts eingetragen (nicht die eines möglichen Docker-Containers))

  • timestamp (wenn nicht gesetzt, wird aktueller Timestamp verwendet; überschreibbar mit UNIX-Timestamp; falls falsch überschrieben, wird die Nachricht verworfen)

Message-IDs sind UUIDs/GUIDs: ee1bdfe2-4c23-11e7-9f76-525400ce74cd.

Nachrichtenversand testen

Wichtig

Taucht in einer JSON-Nachricht an einen Syslog-Input der Key „level“ auf, wird die Nachricht von Graylog verworfen, sobald der dazu gehörende Value nicht kompatibel mit Syslog ist. Beispiel: „level“:“FINE“ wird verworfen, „level“:“5“ dagegen akzeptiert.

Syslog per 1514/udp:

logger --server graylog.example.com --udp --port 1514 "Test Syslog 1514/udp on $(date)"

Um GELF zu testen, wird ncat benötigt.

dnf --assumeyes install nmap-ncat

GELF per 12201/UDP:

# shortest message possible
echo -e '{"message": "Test GELF 12201/udp on '$(date)'"}' \
    | ncat --wait 1 --udp graylog.example.com 12201

# longer message, should split up into fields
echo -e '{
    "version": "1.1",
    "host": "example.org",
    "short_message": "Test GELF 12201/udp on '$(date)'",
    "full_message": "Backtrace here\nsome stuff\nmore stuff",
    "level": 1,
    "_user_id": 9001,
    "some_info": "foo",
    "some_env_var": "bar"
}' | ncat --wait 1 --udp graylog.example.com 12201

GELF per 12201/TCP:

echo -e '{
    "version": "1.1",
    "host": "example.org",
    "short_message": "Test GELF 12201/tcp on '$(date)'",
    "full_message": "Backtrace here\nsome stuff\nmore stuff",
    "level": 1,
    "_user_id": 9001,
    "some_info": "foo",
    "some_env_var": "bar"
}' | ncat --wait 1 graylog.example.com 12201

Tipp

Graylog bei der Arbeit zuschauen: tail --follow /var/log/graylog-server/server.log /var/log/opensearch/*log

Log-Daten senden (Clients)

rsyslog

Soll rsyslog Events an einen Graylog Server weiterleiten, empfiehlt sich das GELF Protokoll, da hier einfach zusätzliche Felder mitgeschickt werden können:

/etc/rsyslog.d/graylog.conf
# send server's FQDN instead of its short hostname
$PreserveFQDN on

template(name="gelf" type="list") {
    constant(value="{\"version\":\"1.1\",")
    constant(value="\"host\":\"")
    property(name="hostname")
    constant(value="\",\"short_message\":\"")
    property(name="msg" format="json")
    constant(value="\",\"timestamp\":")
    property(name="timegenerated" dateformat="unixtimestamp")
    constant(value=",\"level\":\"")
    property(name="syslogseverity")
    constant(value="\"}")
}

# syslog forwarder via UDP
action(type="omfwd" target="graylog.example.com" port="12201" protocol="udp" template="gelf")

Damit ein Client sinnvolle Daten weiterleitet, kann folgende rsyslog-Konfiguration als Basis verwendet werden. So können die Events schon auf dem Client gefiltert werden, und dann per Syslog oder GELF weitergeleitet werden.

/etc/rsyslog.d/graylog.conf
# rsyslog v7 filter conditions:
# contains isequal startswith regex ereregex
# http://www.rsyslog.com/doc/v7-stable/configuration/filters.html
if (
    $msg startswith "GSSAPI client step " or
    $msg startswith "GSSAPI server step " or
    ($programname == "kernel" and $msg startswith "RULE ") or
    ($programname == "systemd" and ($msg startswith "Created slice " or $msg startswith "Removed slice ")) or
    ($programname == "systemd" and ($msg startswith "Starting user-" or $msg startswith "Stopping user-")) or
    ($programname == "systemd" and ($msg startswith "Starting Session " or $msg startswith "Started Session ")) or
    ($programname == "systemd-logind" and ($msg startswith "New Session " or $msg startswith "Removed Session "))
)
then
    # ignore, do not foward
    continue
else
    # Syslog
    *.* @graylog.example.com:1514;RSYSLOG_SyslogProtocol23Format

    # GELF (requires the template from above)
    # action(type="omfwd" target="graylog.example.com" port="12201" protocol="udp" template="gelf")

Beispiel einer Weiterleitung per rsyslog für einen JBoss-Applikationsserver (alternativ WildFly):

/etc/rsyslog.d/jboss.conf
module(load="imfile" PollingInterval="10")

input(
    type="imfile"
    File="/opt/jboss/standalone/log/server.json"
    Tag="jboss-eap"
    StateFile="statefile-to6nnu8DgDug4ImsaaJCpkAQt0zPKb"
)

# just forward, do not log again in /var/log/messages followed by the rules later on in /etc/rsyslog.conf
if $programname == 'jboss-eap' then {
    action(
        type="omfwd"
        Target="graylog.example.com"
        Port="1514"
        Protocol="udp"
    )
    stop
}

Apache

Am besten so: https://www.lisenet.com/2016/send-apache-logs-to-graylog/

  • LogFormat: direkt GELF-kompatibles JSON schreiben

  • CustomLog mit ncat direkt an Graylog pipen

# GELF Log Format
LogFormat "{\n\
  \"version\": \"1.1\",\n\
  \"host\": \"%v\",\n\
  \"short_message\": \"%r\",\n\
  \"timestamp\": %{%s}t,\n\
  \"level\": 6,\n\
  \"_user_agent\": \"%{User-Agent}i\",\n\
  \"_source_ip\": \"%{X-Forwarded-For}i\",\n\
  \"_duration_usec\": %D,\n\
  \"_duration_sec\": %T,\n\
  \"_request_size_byte\": %O,\n\
  \"_http_status_orig\": %s,\n\
  \"_http_status\": %>s,\n\
  \"_http_request_path\": \"%U\",\n\
  \"_http_request\": \"%U%q\",\n\
  \"_http_method\": \"%m\",\n\
  \"_http_referer\": \"%{Referer}i\",\n\
  \"_from_apache\": \"true\"\n\
}" gelf
CustomLog "|/usr/bin/ncat --udp graylog.example.com 12201" gelf

journalctl per GELF

Hinweis: Nachrichten per TCP müssen mit einem „0“ beendet werden.

Empfohlen wird nailgun/journal2gelf. Zunächst EPEL-Repo aktivieren, dann:

dnf --assumeyes install git python-pip gcc python-devel systemd-devel

python3 -m pip install --user git+https://github.com/systemd/python-systemd.git#egg=systemd
python3 -m pip install --user gelfclient
git clone https://github.com/nailgun/journal2gelf.git

cd journal2gelf
python setup.py install

journal2gelf --help

Test (geht per GELF an Port 12201/udp):

journal2gelf graylog.example.com:12201
/etc/systemd/system/journal2gelf.service
[Unit]
Description=Journald to GELF (graylog) log relay service

[Service]
ExecStart=/usr/bin/journal2gelf graylog.example.com:12201
Restart=on-failure
RestartSec=30

[Install]
WantedBy=multi-user.target

Fortinet, Java Application Server

Bei Fortinet-Geräten wird statt Syslog besser das Common Event Format (CEF) verwendet.

Alle Log-Daemons arbeiten nach dem Prinzip „Zeilenumbruch = neue Log-Message“, so auch Graylog. Damit Stacktraces aus einem Java Application Server nicht in dutzende Einzelnachrichten zerfallen, wird im Java-Applikationsserver ein GELF-Appender im nativen, vom Entwickler eingesetzten Java-Logging-Framework (Log4j, Logback etc.) konfiguriert. Viele dieser Appender bauen auf dem GELF Java Client (https://github.com/Graylog2/gelfclient) auf.

Windows

Soll ein Windows-Server Event-Logs senden, wird ein auf Windows zu installierender Agent benötigt. Graylog empfiehlt die Kombi aus WinLogBeat (der eigentliche Client) und Graylog Sidecar (ein Daemon zur Konfiguration des Clients durch den Graylog-Server). Siehe auch https://go2docs.graylog.org/current/getting_in_log_data/ingest_windows_eventlog.html

Auf dem Graylog Server:

  • Für den Sidecar-Benutzer einen API Key unter „System > Users and Teams“ erstellen.

  • Ausserdem muss ein Beats-Input erstellt werden.

Installation von Sidecar auf dem Windows Host:

Zurück auf dem Graylog Server sollte das zuvor installierte Sidecar unter „System > Sidecars“ zu sehen sein. Der Collector auf dem Client muss allerdings noch wissen, welche Daten er sammeln soll - das wird in einer Collector-Configuration festgelegt:

  • System > Sidecars > Configuration

  • Create Configuration

    • Name für die Konfiguration vergeben (z.B. „winlogbeat-default“)

    • Collector: winlogbeat on Windows

    • Tags: Wer den Collector pro Applikation (z.B. für das Sammeln von Apache httpd-logs) konfiguriert, vergibt hier den Tag für die Applikation (also z.B. „Apache httpd“). Für Windows-Systemlogs bietet sich „OS - Windows“ an.

Eine brauchbare Konfiguration wäre (siehe https://go2docs.graylog.org/current/getting_in_log_data/ingest_windows_eventlog.html):

fields_under_root: true
fields.collector_node_id: ${sidecar.nodeName}
fields.gl2_source_collector: ${sidecar.nodeId}

output.logstash:
  # this refers to the Beats input on Graylog
  hosts: ["192.0.2.74:5044"]
    ssl:
      enabled: true
      verification_mode: full

winlogbeat.event_logs:
  - name: Application
    level: critical, error, warning
    ignore_older: 48h
  - name: Security
    processors:
        - drop_event.when.not.or:
            - equals.event_id: 129
            - equals.event_id: 141
            - equals.event_id: 1102
            - equals.event_id: 4648
            - equals.event_id: 4657
            - equals.event_id: 4688
            - equals.event_id: 4697
            - equals.event_id: 4698
            - equals.event_id: 4720
            - equals.event_id: 4738
            - equals.event_id: 4767
            - equals.event_id: 4728
            - equals.event_id: 4732
            - equals.event_id: 4634
            - equals.event_id: 4735
            - equals.event_id: 4740
            - equals.event_id: 4756
    level: critical, error, warning, information
    ignore_older: 48h
  - name: System
    processors:
        - drop_event.when.not.or:
            - equals.event_id: 129
            - equals.event_id: 1022
            - equals.event_id: 1033
            - equals.event_id: 1034
            - equals.event_id: 4624
            - equals.event_id: 4625
            - equals.event_id: 4633
            - equals.event_id: 4719
            - equals.event_id: 4738
            - equals.event_id: 7000
            - equals.event_id: 7022
            - equals.event_id: 7024
            - equals.event_id: 7031
            - equals.event_id: 7034-7036
            - equals.event_id: 7040
            - equals.event_id: 7045
    level: critical, error, warning
    ignore_older: 48h

Jetzt kann die Collector-Configuration dem Client zugewiesen werden:

  • System > Sidecars > Administration

  • winlogbeat aktivieren > Assign Configurations > „winlogbeat-default“ auswählen

Die Konfiguration wird auf dem Client in der Regel unter C:\Program Files\Graylog\sidecar\generated\<RANDOM>\winlogbeat.conf abgelegt, und kann per C:\Program Files\Graylog\sidecar\winlogbeat.exe test config --c generated\...\winlogbeat.conf getestet werden. Die zu Graylog Sidecar gehörenden Windows-Services heissen „Graylog collector sidecar .*“ und „Graylog Sidecar“, und sollten überwacht werden.

Suche und Verarbeitung

Graylog Query Language

Beispiel für eine Suche in zwei Textfeldern und einem Integer-Feld:

NOT requestURI:"/billing/*" AND NOT requestURI:"/calendar/*" AND durationInMs:>500

Hosts ausschliessen:

NOT source:hostA and NOT source:hostB

Das Beispiel

source:host.example.* AND response-time:<30000

liefert auch Ergebnisse mit „response-time“ von 100’000 und mehr? Da drängt sich der Verdacht auf, dass es sich hier um eine Suche nach Strings und nicht nach Integern handelt.

So kann man überprüfen, wie Elasticsearch die Werte speichert - auf dem Log-Server:

curl 'http://localhost:9200/_mapping?pretty'
curl 'http://localhost:9200/_cat/indices?v'
curl 'localhost:9200/_cat/templates?v'

Pipelines

Unter System > Configuration > Message Processors Configuration die „Pipeline“ aktivieren und nach der „Message Filter Chain“ setzen, wenn man auf Felder prüfen möchte, die von Extraktoren gesetzt wurden.

Ablauf:

  • Pipeline anlegen

  • Stage anlegen

  • Rule anlegen

  • Rule zur Stage hinzufügen

  • Pipeline zu Stream connecten

Sinnfreies Beispiel für eine Rule mit Kommentaren, Abfrage auf einen Input und Wegwerfen einer Nachricht:

rule "unneeded if container_name contains _app_ or _cep_"
when
    (
     // to_string($message.gl2_source_input) == "5b16a9176e8bf60b6cf8bc6b" and
     has_field("container_name") and
     (contains(to_string($message.container_name), "_app_") or contains(to_string($message.container_name), "_cep_"))
    )
then
    set_field("unneeded_message", true);
    drop_message();
end

Simulation:

GELF-Message: {
    "version": "1.1",
    "host": "example.org",
    "short_message": "Short message",
    "full_message": "Full message",
    "level": 1,
    "container_name": "my_container"
}

Wert nach Long konvertieren:

rule "Convert RT_FLOW to Numeric"
when
    has_field("bytes-from-server" ) && $message.application_name == "RT_FLOW"
then
    let serverre = to_long( $message.`bytes-from-server`);
    let clientre = to_long( $message.`bytes-from-client`);
    set_field("bytes-from-server_conv", serverre );
    set_field("bytes-from-client_conv", clientre );
end

Events und Alerts

„Alerts“ bedeutet: Man definiert einen Event (also einen Match auf bestimmte Log-Einträge), welcher eine Notification auslöst.

Beispiel: Sende eine Nachricht nach Rocket.Chat, sobald das Feld „operator_alert“ in einer Nachricht in einem bestimmten Stream vorkommt.

  • Graylog GUI > Alerts > Event Definitions > Create event definition

  • Event Details

    • Title: Operator Alert

  • Filter & Aggregation

    • Search Query: _exists_:operator_alert

    • Streams: Stream auswählen

    • Search within the last: 1 hours

    • Execute search every: 5 seconds

  • Notifications

    • Add notification

      • Title: operator_alert Notification

      • Notification Type: Custom HTTP Notification

      • URL: Rocket.Chat Webhook URL

      • Body Template:

        {
          "channel": "my channel",
          "text": ":service_crit: Graylog ${event_definition_title}: ${foreach backlog message}${message.message}${end}"
        }
        
    • Message Backlog: 1

Sicherheit

TLS

Graylog kann so konfiguriert werden, dass die Kommunikation der Logdaten der Clients TLS-verschlüsselt passiert.

Server-Konfiguration

Auf dem Graylog-Server muss beim gewünschten Input Enable TLS aktiviert und die Pfade für das TLS-Zertifikat und den Private-Key bei TLS cert file und TLS private key file hinterlegt werden. Sind Zertifikat und Private-Key im gleichen File vorhanden, so kann auch nur einer der beiden Pfade gesetzt werden.

Das Zertifikat muss den Hostnamen der Inputs entsprechen, d.h. meldet sich ein Client beim Graylog-Server example.com so muss für eine erfolgreiche Kommunikation das konfigurierte Zertifikat entweder den Common Name (CN) oder ein Subject Alternative Name (subjectAltName) für example.com aufweisen.

Sobald bei einem Input Enable TLS aktiv ist, werden nur noch TLS-verschlüsselte Verbindungen akzeptiert. Wenn einige Clients TLS nicht unterstützen, so muss entweder TLS deaktiviert werden oder ein separater, unverschlüsselter Input auf einem anderen Port angelegt werden.

Client-Konfiguration

Clients müssen abhängig vom Nachrichten-Protokoll konfiguriert werden. Werden Graylog Sidecars verwendet, nutzen die Sidecars automatisch TLS.

Ist das Zertifikat nicht von einer Certificate Authority ausgestellt, die im Hostsystem installiert und trusted ist (z.B. self-signed Zertifikate), so kann in der Sidecar-Collector-Konfiguration die CA manuell mit ssl.certificate_authorities gesetzt werden (die CA muss schon auf dem Client vorhanden sein; sie wird nicht per Sidecar verteilt):

...
output.logstash:
    hosts: ["example.com:5044"]
    ssl:
        certificate_authorities: ["/opt/my-example.com-ca.crt"]
        enabled: true
        verification_mode: full
...

Benutzer und Rollen

Benutzer benötigt Decorator-Edit-Rechte? Sowas geht nur über das REST-API.

Verwendbare Berechtigungen abfragen:

curl --request GET \
    --user admin:linuxfabrik \
'http://graylog.example.com:9000/api/system/permissions?pretty=true'

Ergebnis u.a.:

"decorators" : [ "create", "read", "edit" ]

Neue Rolle erstellen:

curl --request POST \
    --user admin:linuxfabrik \
    --header 'Content-Type:application/json' \
    --data '{
        "read_only": false,
        "permissions": ["decorators:create:*", "decorators:read:*", "decorators:edit:*"],
        "name": "Full Decorator Permissions",
        "description": "Permission to create, read or delete decorators."
    }' \
    'http://graylog.example.com:9000/api/roles'

Anschliessend kann die neue Rolle im GUI > Authentication Management ganz unten unter „Roles“ den Benutzern zugeteilt werden.

Berechtigungen für den Benutzer prüfen:

curl --request GET \
    --user admin:linuxfabrik \
    "http://graylog.example.com:9000/api/users/$USERNAME?pretty=true"

API

Alle Methoden auf einen Blick: http://graylog.example.com:9000/api/api-browser

Authentifizierung entweder mittels Username und Passwort, oder per Access Token (letztere ist aus Sicherheitsgründen die bevorzugte Methode).

Access Token erstellen:

  • Benutzer erstellen, Standard-Rolle „Reader“ notfalls anpassen

  • Token zuweisen

curl --user user:linuxfabrik \
    --header 'Accept: application/json' \
    --request GET \
    'http://graylog.example.com:9000/api/...' | jq

Cluster-Informationen auslesen - mit Username und Passwort:

curl --user admin:linuxfabrik \
    --header 'Accept: application/json' \
    'http://graylog.example.com:9000/api/cluster?pretty=true'

Cluster-Informationen auslesen - mit Access Token (Token als Benutzername nutzen; Passwort ist hier immer „token“):

curl --user 1crgamba51dkdn8jsrltpek90m167p1dmteaa0gr7ehscnb3bpce:token \
    --header 'Accept: application/json' \
    'http://graylog.example.com:9000/api/cluster?pretty=true'

Log-Einträge auf Basis einer Query als CSV exportieren:

# urlencoded query
#q=*
q=syslog_identifier%3Asshd%20AND%20opened%20AND%20NOT%20root
range=300       # in seconds

# comma-separated list of fields (%2C)
curl --user admin:linuxfabrik \
    --header 'Accept:text/csv' \
    'http://graylog.example.com:9000/api/search/universal/relative/export?query=$q&range=$range&fields=timestamp%2Csource%2Cmessage' \
    | gzip --best > export.csv

Streams und Stream Rules:

  • Stream Rule Type 1: match exactly

  • Stream Rule Type 2: match regular expression

  • Stream Rule Type 3: greater than

  • Stream Rule Type 4: smaller than

  • Stream Rule Type 5: field presence

  • Stream Rule Type 6: contain

  • Stream Rule Type 7: always match

  • Stream Rule Type 8: match input

curl --user user:linuxfabrik \
    --header 'Accept: application/json' \
    --header 'X-Requested-By: cli' \
    --data '{
          "title": "All messages",
          "description": "All messages are routed here",
          "matching_type": "AND",
          "rules": [
            {
              "field": "",
              "type": 7,
              "inverted": false,
              "value": ""
            }
          ],
          "content_pack": null
        }' \
    --request POST \
    'http://graylog.example.com:9000/api/streams' | jq

Upgrade

Vor dem Upgrade sollte man sich die Upgrading, Release Notes und den Changelog anschauen (Achtung: richtige Version im Dropdown oben auswählen).

Graylog muss inkrementell über jede Minor-Version aktualisiert werden, einzelne Versionen dürfen nicht übersprungen werden. Andernfalls drohen fehlgeschlagene Migrationen oder ein inkonsistenter Systemzustand. Ein Upgrade von 5.2 auf 7.1 erfolgt also schrittweise: 5.2 6.0 6.1 6.2 6.3 7.0 7.1. Bei jedem Zwischenschritt müssen in einem Cluster zuerst alle Nodes auf dieselbe Version gebracht werden, bevor der nächste Schritt folgt. Siehe Incremental Upgrade.

Achtung

MongoDB und self-managed OpenSearch muss immer vor dem Graylog Server auf eine kompatible Version aktualisiert werden. Der Graylog Datanode hingegen sollte nach dem Graylog Server aktualisiert werden.

Graylog empfiehlt kein Rolling Upgrade des Servers, weshalb eine Downtime geplant werden muss (aufgrund möglicher MongoDB-Schema-Migrationen werden keine gemischten Graylog-Server Versionen unterstützt).

Graylog Server aktualisieren:

  1. MongoDB Dump erstellen

  2. Anhand der Compatibility Matrix prüfen, ob MongoDB für die Ziel-Version aktualisiert werden muss; falls ja, MongoDB vor dem Graylog Server aktualisieren (siehe MongoDB)

  3. Alle Graylog Server stoppen: systemctl stop graylog-server.service

  4. Version im Repo-File aktualisieren: $EDITOR /etc/yum.repos.d/graylog.repo

  5. Graylog Server Package aktualisieren: dnf update graylog-server

  6. Config File prüfen, eventuell Änderungen anhand von den Release Notes und den Changelog durchführen: $EDITOR /etc/graylog/server/server.conf

  7. Zuerst den „leader“ Graylog Server Node starten: systemctl start graylog-server.service

  8. Danach alle anderen Graylog Server Nodes starten: systemctl start graylog-server.service

Graylog Datanode aktualisieren:

  • Falls drei oder mehr Datanodes betrieben werden, kann ein Rolling Upgrade ohne jegliche Downtime ausgeführt werden:

    1. Dies kann man im Graylog WebGUI unter System > Cluster Configuration > Data Node Upgrade starten

    2. „Start Upgrade Process“ beim ersten Node anwählen, dann auf dem Node:

      • Graylog Datanode stoppen: systemctl stop graylog-datanode.service

      • Version im Repo-File aktualisieren: $EDITOR /etc/yum.repos.d/graylog.repo

      • Graylog Datanode Package aktualisieren: dnf update graylog-datanode

      • Config File prüfen, eventuell Änderungen anhand von den Release Notes und den Changelog durchführen: $EDITOR /etc/graylog/datanode/datanode.conf

      • Graylog Datanode starten: systemctl start graylog-datanode.service

    3. Nun kann das Upgrade für den Node im WebGUI per „Confirm Upgrade“ bestätigt werden. Die Schritte sind für die weiteren Datanodes analog durchzuführen. Dabei muss man nach jedem Node abwarten, bis die alle Shards alloziert / verteilt wurden. Das kann einen Moment dauern.

  • Falls weniger als drei Datanodes betrieben werden, muss auch hier eine Downtime erwartet werden. Das Upgrade-Vorgehen auf den Nodes ist gleich wie beim Rolling Upgrade.

Backup und Restore

Beim Backup- und Restore-Prozess sind folgende Komponenten zu berücksichtigen:

  • MongoDB (Siehe Backup und Restore)

  • OpenSearch Snapshots (hier beschrieben)

  • Konfiguration (/etc/), Plugins (/usr/share/graylog-server/plugin) und Zertifikate (hier nicht im Detail beschrieben, per file-basiertem Backup sichern, z.B. mittels duplicity). Konkret zu sichern:

Darüber hinaus gilt es, die exakten Versionen von MongoDB sowie Graylog Datanode und Server festzuhalten, damit im Wiederherstellungsfall keine Probleme aufgrund von Versionsunterschieden entstehen.

Bemerkung

Dieser Abschnitt behandelt ausschliesslich dateisystem-basierte Snapshot-Repositories. S3- und andere Repository-Typen sind hier bewusst nicht abgedeckt.

Ablauf für den Graylog Datanode (dateisystem-basiertes Snapshot-Repository):

  1. Repository-Pfad in /etc/graylog/datanode/datanode.conf konfigurieren

  2. Client-Zertifikat von Graylog-Server beziehen

  3. Snapshot-Repository via Datanode-API registrieren

  4. Snapshot anlegen

Repository-Verzeichnis anlegen und Pfad auf Datanode konfigurieren:

snapshot_repo_path='/mnt/opensearch-snapshots'
mkdir --parents "$snapshot_repo_path"
chown graylog-datanode:graylog-datanode "$snapshot_repo_path"
chmod 700 "$snapshot_repo_path"
/etc/graylog/datanode/datanode.conf
path_repo = /mnt/opensearch-snapshots

Warnung

path_repo ist eine kommaseparierte Liste, kein YAML-Array. path_repo = ["/path/to/snapshot/repo"] wird vom Datanode stillschweigend akzeptiert, das eingebettete OpenSearch scheitert danach mit einer unklaren Fehlermeldung (siehe Graylog Issue #22478). Mehrere Pfade müssen so angegeben werden: path_repo = /pfad/a,/pfad/b.

Bemerkung

Der Datanode generiert opensearch.yml bei jedem Start selbst und übersetzt path_repo in die OpenSearch-Einstellung path.repo. opensearch.yml daher nie direkt editieren, manuelle Änderungen werden überschrieben.

Sobald ein Repository konfiguriert ist, ergänzt der Datanode die search-Rolle der Node automatisch und reserviert node_search_cache_size (Default 10gb).

Im Cluster muss das Repository-Verzeichnis auf allen Datanodes derselbe gemeinsame Speicher sein (z.B. NFS), am identischen Pfad gemountet. Ein rein lokales Verzeichnis funktioniert nur bei einem Single-Node-Datanode.

systemctl restart graylog-datanode.service

Client-Zertifikat von Graylog-Server beziehen. Der Endpunkt POST /api/ca/clientcert erwartet je nach Graylog-Version leicht unterschiedliche Argumente: certificate_lifetime existiert seit 6.3, das Einzelfeld role wurde in 7.0 durch die Liste roles ersetzt (role funktioniert dort weiterhin, gilt aber als veraltet), und password darf erst ab 7.0 null sein. Die drei folgenden Request-Bodies entsprechend passend auswählen.

graylog_url='http://localhost:9000'
graylog_admin_username='admin'
read -s graylog_admin_password

clientcert_base_path='/tmp/graylog-clientcert'
clientcert_lifetime='P30Y' # 30 years
clientcert_principal="$(hostname --fqdn)" # fqdn of datanode where cert will be stored
clientcert_role='manage_snapshots'


clientcert_path="$clientcert_base_path/$clientcert_principal"
mkdir --parents "$clientcert_path"

# Graylog 6.1
clientcert_request="$(cat <<EOF
{
    "password": "",
    "principal": "$clientcert_principal",
    "role": "$clientcert_role"
}
EOF
)"

# Graylog 6.3
clientcert_request="$(cat <<EOF
{
    "certificate_lifetime": "$clientcert_lifetime",
    "password": "",
    "principal": "$clientcert_principal",
    "role": "$clientcert_role"
}
EOF
)"

# Graylog 7.0
clientcert_request="$(cat <<EOF
{
    "certificate_lifetime": "$clientcert_lifetime",
    "password": null,
    "principal": "$clientcert_principal",
    "roles": ["$clientcert_role"]
}
EOF
)"

clientcert_response="$(curl --silent \
    --request POST "$graylog_url/api/ca/clientcert" \
    --header 'Content-Type: application/json' \
    --header 'X-Requested-By: cli' \
    --basic --user "$graylog_admin_username:$graylog_admin_password" \
    --data "$clientcert_request")"

jq --raw-output '.ca_certificate' <<<"$clientcert_response" >"$clientcert_path/ca.crt"
jq --raw-output '.private_key' <<<"$clientcert_response" >"$clientcert_path/private.key"
jq --raw-output '.certificate' <<<"$clientcert_response" >"$clientcert_path/private.crt"

Snapshot-Repository im Datanode registrieren:

datanode_url="https://localhost:9200"
snapshot_repo_path='/mnt/opensearch-snapshots'
snapshot_repo_name='opensearch-snapshots'

auth=(
    --cacert "$clientcert_path/ca.crt"
    --key "$clientcert_path/private.key"
    --cert "$clientcert_path/private.crt"
)

# register filesystem-based snapshot repository
snapshot_repo_request="$(cat <<EOF
{
    "type": "fs",
    "settings": {
        "location": "$snapshot_repo_path"
    }
}
EOF
)"

curl --silent "${auth[@]}" \
    --header 'Content-Type: application/json' \
    --request PUT "$datanode_url/_snapshot/$snapshot_repo_name" \
    --data "$snapshot_repo_request" | jq

# list registered snapshot repositories
curl --silent "${auth[@]}" \
    --header 'Content-Type: application/json' \
    --request GET "$datanode_url/_snapshot/_all" | jq

# verify the repository is reachable from all nodes
curl --silent "${auth[@]}" \
    --header 'Content-Type: application/json' \
    --request POST "$datanode_url/_snapshot/$snapshot_repo_name/_verify" | jq

Ein erfolgreicher Verify listet alle Nodes auf, die das Repository erreichen können. Jeder Eintrag unter nodes steht für eine Node, die lesend und schreibend auf das Repository zugreifen kann. Auf einem Single-Node-Datanode ist das genau eine Node, in einem Cluster müssen hier alle Datanodes erscheinen. Kann eine Node das Repository nicht erreichen (Pfad fehlt, fehlende Rechte, NFS nicht gemountet), liefert der Aufruf stattdessen einen Fehler.

{
  "nodes": {
    "2fK4KxqVQvSomutOqgVg0Q": {
      "name": "localhost"
    }
  }
}

Der Schlüssel (hier 2fK4KxqVQvSomutOqgVg0Q) ist die interne Node-ID, name der Node-Name (node.name).

Nun kann man Snapshots anlegen. Ein Stoppen des Graylog-Servers ist dafür nicht nötig: OpenSearch-Snapshots sind Point-in-Time-Kopien der unveränderlichen Lucene-Segmente und laufen im laufenden Betrieb. Wer ein lückenloses Voll-Backup vor einer Migration anlegen will, kann die Ingestion vorher stoppen; für das Sichern einzelner, insbesondere bereits read-only gesetzter Indizes ist das unnötig. Statt aller Indizes ("indices": "*") lässt sich auch eine Teilmenge sichern, etwa "indices": "graylog_0".

# snapshot all indices
curl --silent "${auth[@]}" \
    --request PUT "$datanode_url/_snapshot/$snapshot_repo_name/snapshot-1?wait_for_completion=true" \
    --header 'Content-Type: application/json' \
    --data '{
        "indices": "*",
        "ignore_unavailable": true,
        "include_global_state": false
    }' | jq

Manuell, also ohne SM-Policy, angelegte Snapshots werden über die OpenSearch-API gelöscht. Zuerst die Snapshots im Repository auflisten, dann den gewünschten löschen:

# list snapshots in the repository
curl --silent "${auth[@]}" \
    --request GET "$datanode_url/_cat/snapshots/$snapshot_repo_name?v&s=id"

# delete one snapshot
curl --silent "${auth[@]}" \
    --request DELETE "$datanode_url/_snapshot/$snapshot_repo_name/snapshot-1" | jq

Bemerkung

Da Snapshots inkrementell sind, gibt das Löschen nur jene Segmente frei, die kein anderer Snapshot im Repository mehr referenziert; gemeinsam genutzte Daten bleiben erhalten. Von einer SM-Policy verwaltete Snapshots nicht von Hand löschen, dafür ist deren Deletion-Schedule zuständig.

Statt Snapshots manuell anzulegen, übernimmt das die Snapshot-Management-Funktion (SM) von OpenSearch: ein Zeitplan erzeugt regelmässig Snapshots und entfernt alte nach Aufbewahrungsregeln.

Hier am Beispiel vom folgenden Use-Case: Das Snapshot-Repository liegt unter /mnt/opensearch-snapshots und wird von einem externen Backup-Tool täglich gesichert. Das SM-Repository ist damit die schnelle, cluster-lokale Restore-Stufe, die Langzeit-Aufbewahrung übernimmt das externe Backup-Tool. Die SM-Retention darf entsprechend kurz sein.

sm_policy_request="$(cat <<EOF
{
    "description": "Daily Graylog snapshot, short-term on-cluster retention",
    "creation": {
        "schedule": { "cron": { "expression": "0 2 * * *", "timezone": "Europe/Zurich" } }
    },
    "deletion": {
        "schedule": { "cron": { "expression": "30 2 * * *", "timezone": "Europe/Zurich" } },
        "condition": { "max_count": 2, "min_count": 1 }
    },
    "snapshot_config": {
        "repository": "$snapshot_repo_name",
        "indices": "*",
        "ignore_unavailable": "true",
        "include_global_state": "false",
        "date_format": "yyyy-MM-dd-HH-mm",
        "timezone": "Europe/Zurich"
    }
}
EOF
)"

curl --silent "${auth[@]}" \
    --request PUT "$datanode_url/_plugins/_sm/policies/graylog-daily" \
    --header 'Content-Type: application/json' \
    --data "$sm_policy_request" | jq

Die Policy ist nach dem Anlegen aktiv. Status, letzte und nächste Ausführung lassen sich so prüfen:

curl --silent "${auth[@]}" \
    --request GET "$datanode_url/_plugins/_sm/policies/graylog-daily/_explain" | jq

Aufbewahrung: Der Zeitplan erzeugt täglich um 02:00 einen Snapshot. SM kann nicht exakt einen Snapshot vorhalten: min_count muss grösser als 0 sein (Standard 1) und max_count muss grösser als min_count sein. Das Minimum ist daher max_count: 2 bei min_count: 1, es bleiben der neueste plus ein vorheriger Stand erhalten. Da Snapshots inkrementell sind, kostet der zweite Stand kaum zusätzlichen Platz. Die Versionshistorie übernimmt ohnehin das externe Backup-Tool, das /backup täglich sichert; lokal genügen diese ein bis zwei aktuellen Restore-Punkte.

Bemerkung

Der zeitliche Abstand zwischen Creation und Deletion ist für die Korrektheit unkritisch. Läuft die Snapshot-Erstellung noch, wenn die Deletion feuert (z.B. weil der erste, vollständige Snapshot länger als 30 Minuten dauert), überspringt SM den gerade entstehenden Snapshot: er wird im Zustand IN_PROGRESS herausgefiltert und nicht gegen max_count gezählt. In dieser Runde wird dann nichts gelöscht, die überzählige Sicherung räumt der nächste Deletion-Lauf weg. Es bleibt also kurzzeitig ein Stand mehr liegen, es gehen aber keine Daten verloren und es entsteht kein Konflikt.

Bemerkung

include_global_state: false ist bewusst gewählt (gilt auch für den manuellen Snapshot oben). Pro-Index-Daten und Mappings liegen ohnehin im Index-Teil des Snapshots und werden unabhängig von diesem Flag wiederhergestellt. Der globale Cluster-Status (Index-Templates, Ingest-Pipelines, Cluster-Settings) wird dagegen von Graylog beim Start neu gesetzt, und die massgebliche Graylog-Konfiguration liegt in MongoDB und wird separat gesichert. Würde man alten globalen Status mitsichern und zurückspielen, drohte ein Überschreiben aktueller Cluster-Einstellungen mit veralteten Werten. true lohnt nur bei einer Bare-Cluster-Disaster-Recovery ohne separates MongoDB-Backup.

Bemerkung

Den SM-Zeitplan so legen, dass der Snapshot abgeschlossen ist, bevor das externe Backup-Tool /backup kopiert, sonst sichert das Tool einen unfertigen Snapshot mit. Bereits abgeschlossene Snapshots im Repository bleiben dabei konsistent; lediglich ein im Moment der Kopie noch laufender Snapshot landet unvollständig im externen Backup und ist dort unbrauchbar. Das externe Tool darf /backup nur lesen, niemals selbst in das Repository schreiben.

Restore

Beim Restore gibt es zwei Varianten. Die Wahl hängt davon ab, ob der bestehende Index ersetzt werden soll oder die gesicherten Daten zusätzlich, parallel zu den bestehenden, bereitstehen sollen.

OpenSearch-Randbedingung: Ein Snapshot lässt sich nicht in einen bereits geöffneten Index mit gleichem Namen wiederherstellen. Ein vorhandener gleichnamiger Index muss vorher geschlossen oder gelöscht werden.

Variante A, abweichender Name (nicht-destruktiv, im laufenden Betrieb): Die gesicherten Indizes werden unter neuem Namen (hier mit Präfix restored_) zusätzlich zu den bestehenden eingespielt. Der Graylog-Server muss nicht gestoppt werden, da Graylog diese Indizes nicht verwaltet. Geeignet, um Daten zu prüfen oder einzelne Inhalte zu retten.

# restore alongside the originals under a different name; graylog-server keeps running
curl --silent "${auth[@]}" \
    --request POST "$datanode_url/_snapshot/$snapshot_repo_name/snapshot-1/_restore?wait_for_completion=true" \
    --header 'Content-Type: application/json' \
    --data '{
        "indices": "graylog_0",
        "rename_pattern": "(.+)",
        "rename_replacement": "restored_$1",
        "include_global_state": false
    }' | jq

rename_pattern ist ein regulärer Ausdruck auf die Index-Namen im Snapshot, rename_replacement der Ziel-Name ($1 referenziert die erste Capture-Gruppe). Damit Graylog die so wiederhergestellten Daten durchsuchen kann, müssten sie einem Index-Set zugeordnet und die Index-Ranges neu berechnet werden; für reines Sichten oder Extrahieren ist das nicht nötig.

Variante B, originaler Name (destruktiv, ersetzt den Index): Der bestehende Index wird durch den Snapshot-Stand ersetzt. Da Graylog diesen Index aktiv verwaltet, muss der Graylog-Server gestoppt sein, sonst drohen Inkonsistenzen bei Index-Ranges, Retention oder Rotation. Der Ablauf ist somit: Graylog-Server stoppen, alten Index löschen, Snapshot unter Originalnamen einspielen, Graylog-Server starten, Index-Ranges neu berechnen.

Warnung

Diese Variante eignet sich nur für alte, bereits rotierte Indizes, nicht für den aktiven Schreib-Index. Graylog schreibt alle neuen Nachrichten über den Alias <prefix>_deflector (z.B. graylog_deflector), der stets auf den aktuellen, höchstnummerierten Schreib-Index zeigt (z.B. graylog_5). Wird dieser Index unter seinem Originalnamen gelöscht und wiederhergestellt, zeigt der Alias ins Leere, laufende Schreibvorgänge schlagen fehl und Graylog gerät in einen inkonsistenten Zustand.

Muss der aktive Index ersetzt werden, muss vorher den Schreib-Index rotiert werden (System -> Indices -> Index-Set öffnen -> Maintenance -> Rotate active write index). Graylog legt dann einen neuen Schreib-Index an (z.B. graylog_6) und schwenkt den Deflector dorthin. Der bisherige Index (graylog_5) wird zum inaktiven, read-only gesetzten Alt-Index und damit zu einem sicheren Restore-Ziel.

Die Aliase können per curl "${auth[@]}" "https://localhost:9200/_cat/aliases?v&s=alias" geprüft werden.

systemctl stop graylog-server

# delete the existing index so the snapshot can restore under the same name
curl --silent "${auth[@]}" \
    --request DELETE "$datanode_url/graylog_0" | jq

# restore under the original name
curl --silent "${auth[@]}" \
    --request POST "$datanode_url/_snapshot/$snapshot_repo_name/snapshot-1/_restore?wait_for_completion=true" \
    --header 'Content-Type: application/json' \
    --data '{
        "indices": "graylog_0",
        "include_global_state": false
    }' | jq

systemctl start graylog-server

Nach dem Start müssen die Index-Ranges neu berechnet werden: System -> Indices -> Index-Set öffnen -> betroffenen Index aufklappen -> Button „Recalculate index ranges“. Für ein ganzes Index-Set steht die Aktion stattdessen unter Maintenance -> Recalculate index ranges des Index-Sets.

Bemerkung

Statt den alten Index zu löschen, genügt OpenSearch auch ein Schliessen (POST /graylog_0/_close). Ein Löschen liefert aber einen sauberen, vollständigen Ersatz und ist daher vorzuziehen. Das Löschen eines Index wird durch index.blocks.write nicht blockiert, der read-only-Schreibblock alter Indizes muss dafür also nicht aufgehoben werden.

Migration zu Graylog Data Node

Seit Graylog 6.0 bietet Graylog die Data Node als offiziell gepflegte, in Graylog integrierte OpenSearch-Instanz an. Wer bis dahin eine eigenständige OpenSearch-Installation betreibt, kann entweder dabei bleiben (externe OpenSearch wird weiterhin supportet) oder auf die Data Node migrieren. Offiziell dokumentiert ist dafür ausschliesslich die In-Place Migration; alles andere sind pragmatische Vorgehen aus der Praxis.

In-Place Migration (offiziell)

Der Graylog-Assistent in der Web-UI führt durch die Schritte. Daten werden dabei nicht kopiert: der Data Node übernimmt das bestehende OpenSearch-Datenverzeichnis und startet darauf weiter. Voraussetzung ist OpenSearch 1.13 oder neuer, bis einschliesslich 2.19.4; Elasticsearch-Quellen werden für In-Place nicht unterstützt. Rollback geht nur auf Cluster-Ebene, OpenSearch-Binaries und eine Dateisystem-Kopie des Datenverzeichnisses sollten bis zur Verifikation behalten werden.

Kompatibilitäts-Eckdaten laut Graylog Compatibility Matrix (Stand 2026-04):

Graylog

Unterstützte OpenSearch-Version (extern oder als Data Node)

6.1.x

1.1.x bis 2.15.x

6.2.x, 6.3.x, 7.0.x

1.1.x bis 2.19.4

Graylog 7.x unterstützt OpenSearch 3.x nicht. Vor jedem Minor-Upgrade die Matrix gegenchecken.

Snapshot/Restore (inoffiziell)

OpenSearch-Indices werden per Snapshot-Repository (NFS, S3, o.ä.) gesichert und in den Data-Node-Cluster zurückgespielt. Die alte OpenSearch bleibt aktiv, bis bestätigt ist, dass alle Indices im Data-Node-Cluster vorhanden, abfragbar und von Graylog ansprechbar sind. Nützlich bei gleichzeitiger Host-Migration oder wenn die In-Place-Variante wegen Pfad- oder Versions-Constraints nicht greift. Graylog dokumentiert Snapshot/Restore nur als generelles OpenSearch-Backup (siehe OpenSearch Backup & Restore), nicht als offiziellen Migrationspfad zur Data Node. Lucene-Versionskompatibilität zwischen Quell- und Ziel-Cluster vor der Migration prüfen und in einer Testumgebung durchspielen.

Side-by-Side (inoffiziell)

Ein zweites Graylog mit Data Node wird parallel aufgebaut und die Index-Sets stückweise auf den neuen Stack umgezogen. Graylog verwaltet pro Instanz nur einen Indexer-Cluster, echte zwei aktive Search-Backends unter einem Graylog gibt es nicht. Die Side-by-Side-Variante läuft damit faktisch auf zwei getrennte Graylog-Instanzen hinaus, mit entsprechend höherem Aufwand bei Log-Routing, Stream-Regeln und Forwarder-Umstellung. In der Praxis eher eine Notlösung für Umgebungen, in denen ein harter Cutover keine Option ist.

In-Place-Migration Schritt für Schritt

Konkrete Anleitung für die offizielle In-Place-Migration eines bestehenden, unverschlüsselten OpenSearch-Clusters (plugins.security.disabled: true, siehe OpenSearch) auf Graylog Data Nodes. Der Data Node übernimmt das vorhandene OpenSearch-Datenverzeichnis weiter, Daten werden nicht kopiert. Läuft das OpenSearch mit aktivem Security-Plugin und TLS, greift stattdessen die TLS-Secured Data Node Migration mit zusätzlicher JWT-Authentifizierung.

Achtung

Das Graylog-Server-Cluster muss vor der Migration vollständig auf der Zielversion laufen (In-Place offiziell ab Graylog 6.1, hier auf 7.0). Graylog 7.0 setzt MongoDB 7.0, Java 21 voraus. Der inkrementelle Upgrade-Pfad inklusive MongoDB ist im Abschnitt Upgrade beschrieben. Erst migrieren, wenn alle Server-Nodes auf derselben 7.0-Version laufen.

Voraussetzungen laut Data Node In-Place Migration:

  • alle Server-Nodes auf der Zielversion (siehe Upgrade)

  • OpenSearch zwischen 1.13 und 2.19; Elasticsearch-Quellen werden nicht unterstützt

  • gültige Backups von MongoDB und OpenSearch (siehe Backup und Restore)

  • Data Node wird auf denselben Hosts wie das bestehende OpenSearch installiert

Preflight - auf jedem OpenSearch-Node prüfen und sichern:

  1. Freien Plattenplatz prüfen: mindestens 4 GB auf / und 15 GB auf /var.

  2. Alle relevanten Versionen festhalten (OpenSearch, Graylog, Java, MongoDB)

  3. Aktuellen Datenbestand sowie Index- und Shard-Zahl erfassen:

    curl 'http://localhost:9200/_cat/indices?v'
    curl 'http://localhost:9200/_cluster/health?pretty'
    
  4. MongoDB und OpenSearch sichern (siehe Backup und Restore).

  5. Sicherstellen, dass der Paket-Mirror das graylog-datanode-Paket in der zur Server-Version passenden Version bereitstellt.

  6. Indizes ohne Replica aufspüren - beim Stoppen eines OpenSearch-Nodes gingen deren Shards sonst verloren. Indizes mit number_of_replicas: 0 auflisten:

    curl --cacert "$elastic_cacert" \
        --user "elastic:${ELASTIC_PASSWORD}" \
        --request GET "https://$elastic_host:9200/*/_settings?expand_wildcards=all&filter_path=*.settings.index.number_of_replicas" \
        --header "Content-Type: application/json" \
        | jq 'to_entries[] | select(.value.settings.index.number_of_replicas == "0") | .key'
    

    Replica-Zahl für die betroffenen Index-Sets hochsetzen (verlangt mindestens zwei Nodes; ein Single-Node-Cluster bleibt sonst gelb):

    curl --silent --request PUT 'http://localhost:9200/graylog_*/_settings' \
        --header 'Content-Type: application/json' \
        --data '{"index": {"number_of_replicas": 1}}'
    
    curl --silent --request PUT 'http://localhost:9200/gl-system-events*/_settings' \
        --header 'Content-Type: application/json' \
        --data '{"index": {"number_of_replicas": 1}}'
    

Bemerkung

Die offizielle In-Place-Anleitung geht von einem Quell-OpenSearch mit aktivem Security-Plugin aus und enthält daher einen JWT-Schritt (Eintrag in opensearch-security/config.yml, signing key = base64-kodiertes GRAYLOG_PASSWORD_SECRET), der dem Data Node die Autorisierung zum Daten-Import gibt. Läuft das Quell-OpenSearch mit plugins.security.disabled: true (Plugin aus, unverschlüsselt), entfällt dieser Schritt - es gibt keine opensearch-security/config.yml und keine Auth, gegen die authentifiziert werden müsste. Das untenstehende Runbook beschreibt diesen unverschlüsselten Fall; er ist offiziell nicht eigens dokumentiert und sollte vorab in einer Testumgebung durchgespielt werden. Bei aktivem Security-Plugin stattdessen der TLS-Secured Data Node Migration folgen.

Reihenfolge nach Data Node In-Place Migration. Vorbereitung ausserhalb der Web-UI:

  1. Data Node auf jedem OpenSearch-Node installieren, entweder mit dem LFOps oder manuell (Graylog-Repo ist bereits eingerichtet, siehe Installation):

    dnf --assumeyes install graylog-datanode
    dnf versionlock add graylog-datanode
    
  2. /etc/graylog/datanode/datanode.conf konfigurieren. Die Anleitung nennt nur opensearch_data_location; für den Betrieb des Data Node werden ausserdem password_secret und mongodb_uri gesetzt:

    • opensearch_data_location auf das bestehende OpenSearch-Datenverzeichnis (path.data, im Standard /var/lib/opensearch)

    • password_secret identisch zum password_secret aus der server.conf

    • mongodb_uri auf die vorhandene MongoDB

Im Migrations-Assistenten (Web-UI):

  1. Assistenten öffnen: System > Cluster Configuration > Data Node Migration.

  2. Interne Graylog-CA erzeugen (empfohlen) oder eigene CA (PEM oder PKCS#12) hochladen und die Renewal-Policy wählen (Automatic als Default, 30 Tage Zertifikatslaufzeit).

  3. „Run directory compatibility check“ ausführen. Der Wizard prüft die OpenSearch-Version, sucht nach Nicht-Graylog-Daten und zählt die Indizes - das OpenSearch läuft dabei noch.

  4. Data-Node-Zertifikate provisionieren („Provision Data Nodes with certificates“).

  5. Journal-Downtime-Warnung bestätigen. Der Server muss eingehende Nachrichten puffern können, während das Search-Backend während der Migration nicht erreichbar ist.

Abschluss:

  1. OpenSearch auf allen Nodes stoppen. Die Anleitung stoppt nur; ein zusätzliches disable verhindert einen versehentlichen Neustart:

    systemctl disable --now opensearch.service
    
  2. Eigentümer des Datenverzeichnisses auf den Data-Node-Service umstellen, sonst kann der Data Node die Daten nicht lesen (das Verzeichnis gehört opensearch, der Dienst läuft als graylog-datanode). Die TLS-Secured-Anleitung dokumentiert den Schritt wörtlich:

    chown --recursive graylog-datanode:graylog-datanode /var/lib/opensearch
    
  3. elasticsearch_hosts in /etc/graylog/server/server.conf auf allen Server-Nodes deaktivieren, damit Graylog kein eigenständiges Search-Backend mehr sucht.

  4. Warten, bis alle Data Nodes den Status AVAILABLE erreichen (kann einige Minuten dauern), dann den Graylog-Server neu starten:

    systemctl restart graylog-server.service
    
  5. Den Assistenten mit „Next“ abschliessen.

  6. Die Indizes, bei denen man die Replica-Zahl auf 1 gesetzt hat, wieder auf 0 setzen

    curl --silent --request PUT 'http://localhost:9200/graylog_*/_settings' \
        --header 'Content-Type: application/json' \
        --data '{"index": {"number_of_replicas": 0}}'
    

Nach der Migration prüfen (das vom Data Node gemanagte OpenSearch läuft nun mit TLS, deshalb über die UI statt über http://...:9200):

  • System > Data Nodes: alle Nodes grün.

  • System > Indices: alle Index-Sets vorhanden, Shard-Zahl mindestens so hoch wie vor der Migration.

  • System > Nodes: Verarbeitung aktiv, Journal-Auslastung sinkt wieder.

  • Bei index_not_found_exception: System > Indices > Maintenance > „Cleanup & recalculate all index ranges“.

  • graylog-server und graylog-datanode per dnf versionlock gegen ungewollte Updates pinnen (MongoDB ist bereits gepinnt, siehe Installation).

Bis die Migration verifiziert ist, OpenSearch-Binaries und eine Dateisystem-Kopie des Datenverzeichnisses behalten - ein Rollback ist nur auf Cluster-Ebene möglich.

Endzustand: Der Data Node bringt sein eigenes, von Graylog gemanagtes OpenSearch mit (/usr/share/graylog-datanode/dist); ein separates, security-deaktiviertes OpenSearch gibt es danach nicht mehr. HTTP- und Transport-Layer laufen über automatisch provisionierte Zertifikate (TLS), die Kommunikation zwischen Graylog-Server und OpenSearch ist zusätzlich per JWT abgesichert (geteiltes password_secret). Die Discovery läuft über die gemeinsame MongoDB: Der Data Node registriert sich dort, der Graylog-Server findet ihn darüber - elasticsearch_hosts wird dann nicht mehr manuell gepflegt. Das ist der Bruch zum bisherigen Setup mit externem OpenSearch.

Best Practices

  • Pipelines gegenüber Extractors bevorzugen. Pipelines werden pro Stream angewendet und bieten einen Simulator zum Testen; Extractors sind an einen einzelnen Input gebunden. Für neue Parsing-Logik ist die Pipeline in der Regel die flexiblere Option. Konkrete Empfehlung der Graylog-Community in der Upstream-Doku vor einer Umstellung gegenchecken.

  • Separate Streams für separate Retention. Security-Logs sollen oft länger aufbewahrt werden als z.B. Apache-Access-Logs. Separate Index Sets mit eigener Retention-Policy konfigurieren und entsprechende Streams darauf zeigen lassen.

  • OpenSearch-Heap bei 50 % RAM, maximal 31 GB. JVM kann darüber hinaus wegen Compressed-OOPs keinen effektiven Nutzen mehr ziehen. Restlicher RAM geht in den Filesystem-Cache - entscheidend für die OpenSearch-Leseperformance.

  • Sidecars für Logsammlung statt direkter Syslog-Forwarding-Konfigurationen auf den Hosts. Beats-basierte Collectors sind robuster gegen Netzwerkausfälle und können lokal buffern, wenn Graylog nicht erreichbar ist.

  • TLS auf allen Inputs. Besonders GELF-TCP und Beats sollten mit TLS verschlüsselt werden; der Beats-Input in Graylog unterstützt Client-Zertifikat-Authentifizierung.

  • Graylog-Admin-Account absichern. Nach der Erstinstallation einen dedizierten Admin-Account anlegen (z.B. via LDAP-Anbindung oder OIDC - letzteres in der OSS- Version nur eingeschränkt, siehe Feature-Liste oben) und den hardcodierten admin-Account via root_password_sha2 in server.conf deaktivieren.

  • Index-Rotation überwachen. Graylog rotiert Indices nach Zeit, Grösse oder Message-Anzahl. Indices & Index Sets prüft, ob alle Indices grün sind; rote oder gelbe Shards bedeuten Datenverlust-Risiko.

Troubleshooting

„415 Unsupported Media Type“ bei Verwendung des APIs

Der Server lehnt die Anfrage ab, weil der Request-Body in einem Format vorliegt, das die angefragte Ressource für diese Methode nicht unterstützt. Meist fehlt der Header Content-Type: application/json.

OpenSearchException[OpenSearch exception [type=illegal_argument_exception, reason=mapper [latency] cannot be changed from type [float] to [long]]]

Die Mappings (OpenSearch / Elasticsearch Felder und deren Typ) werden dynamisch anhand der ersten Nachricht im jeweiligen Index bestimmt. Dies geschieht jedes Mal, wenn ein neuer Index begonnen wird, also auch, wenn der Index rotiert wurde. Das heisst, wenn der erste Eintrag latency: 0 ist, nimmt OpenSearch fälschlicherweise an, dass es sich um ein long handelt. Wenn man nun einen float-Wert schickt, wird dieser zwar im Graylog und in OpenSearch als float angezeigt, intern aber trotzdem als long behandelt (siehe https://xeraa.net/blog/2020_elasticsearch-coerce-float-to-integer-or-long/).

Um dieses Problem zu beheben, kann man den Typ für das Feld latency manuell setzen:

# show current mapping
curl 'localhost:9200/lfops-default_4/_mappings?pretty' | grep latency -A1
# "latency" : {
#   "type" : "float"

# create custom mapping, see https://go2docs.graylog.org/current/setting_up_graylog/elasticsearch.htm#CustomIndexMappings
cat > lfops-latency-float.json << 'EOF'
{
    "template": "lfops-default_*",
    "order": 10,
    "mappings": {
        "properties": {
            "latency": {
                "type": "float"
            }
        }
    }
}
EOF
curl --request PUT \
    --data @'lfops-latency-float.json' \
    --header 'Content-Type: application/json' \
    'http://localhost:9200/_template/lfops-latency-float?pretty'

# show current templates
curl 'http://localhost:9200/_template?pretty'

Anschliessend muss im Graylog unter Indices der betroffene Index ausgewählt und über „Maintenance > Rotate active write index“ der aktuelle Index neu generiert werden.

Datanodes werden bei der Installation nicht gefunden?

Prüfen, ob sie in der MongoDB eingetragen sind

mongosh
use graylog
db.datanodes.find()
Zertifikats-Probleme mit einem Datanode Cluster?

Prüfen, ob sich alle Datanodes untereinander via DNS erreichen können (auch Firewall beachten). Besonders wichtig: Auch die /etc/hosts prüfen - 127.0.0.1 bzw. ::1 dürfen ausschliesslich auf die localhost-Varianten zeigen. Falls zusätzlich der eigentliche Hostname angegeben wird, schlägt das Ausstellen der Zertifikate mit dem Fehler certificate_unknown fehl.

WARN  [MongodbNodesProvider] Could not get MongodbNodes, returning fallback. Reason: Command execution failed on MongoDB server with error 13 (Unauthorized): 'not authorized on admin to execute command { replSetGetStatus: 1, $db: "admin", ...

Seit Graylog 7.1 braucht der MongoDB-User für Graylog zusätzlich die clusterMonitor-Rolle (für die „MongoDB Nodes“-Sektion unter System > Cluster Configuration). Manuell per mongosh:

mongosh
use graylog
db.grantRolesToUser("graylog", [{ role: "clusterMonitor", db: "admin" }])

Oder per LFOps-Rolle linuxfabrik.lfops.mongodb:

mongodb__users__group_var:
  - username: 'graylog'
    password: 'linuxfabrik'
    database: 'graylog'
    roles:
      # clusterMonitor is required for graylog 7.1+, for the "MongoDB Nodes" section in the System > Cluster Configuration
      - db: 'admin'
        role: 'clusterMonitor'
      - db: 'graylog'
        role: 'readWrite'
      - db: 'graylog'
        role: 'dbAdmin'
    state: 'present'