Sitemap
Onix.09

Onix.09

Graylag Installation & Wazuh Integration Guide

13 min readOct 20, 2026

--

Oct 8, 2026 · Onix.09

Overview

Graylog 7.1 runs in Docker on the Wazuh server and becomes the central log store. Wazuh keeps doing detection. Fluent Bit tails Wazuh’s alerts.json and streams each alert to Graylog. A pipeline rule then turns the JSON into searchable fields.

Press enter or click to view image in full size

data flow · Wazuh → Fluent Bit → Graylog

Wazuh’s own indexer and dashboard keep running unchanged; Graylog receives a copy of every alert.

Press enter or click to view image in full size

All files live in /home/analyst/graylog/: docker-compose.yml, .env (secrets, mode 600), fluent-bit.conf and install.sh.

Prerequisites

The Wazuh server (10.10.32.19) has enough headroom to host Graylog next to Wazuh. These values were checked on the host on 2026–10–08.

Press enter or click to view image in full size

Port plan. Wazuh already owns 443, 1514, 1515, 55000, 9200 and 9300. So the Graylog Data Node keeps 9200/9300 inside the Docker network and is never published to the host.

Press enter or click to view image in full size

Check for port conflicts before installing:

cat /etc/os-release | head -4; nproc; free -h; df -h /
sysctl vm.max_map_count
systemctl is-active wazuh-manager wazuh-indexer wazuh-dashboard
ss -tlnu | grep -E ‘:(9000|5555|5140|12201|8999)\b’ || echo “ports free”

Install Graylog

One root script installs Docker, starts the Graylog stack and installs Fluent Bit. On this host it completed in about 3 minutes: Docker 29.8.2, Compose 5.6.0 and Fluent Bit 5.1.3.

1. Create the project folder and secrets

GRAYLOG_PASSWORD_SECRET peppers stored passwords and must be at least 64 characters. GRAYLOG_ROOT_PASSWORD_SHA2 is the SHA-256 of the admin password. Both go in .env, which only the owner can read.

mkdir -p ~/graylog && cd ~/graylog && umask 077
SECRET=$(head -c 96 /dev/urandom | base64 | tr -dc ‘A-Za-z0–9’ | head -c 96)
ADMINPW=$(head -c 32 /dev/urandom | base64 | tr -dc ‘A-Za-z0–9’ | head -c 20)
printf ‘%s\n’ “$ADMINPW” > .admin_password # keep this safe
HASH=$(printf ‘%s’ “$ADMINPW” | sha256sum | cut -d’ ‘ -f1)
printf ‘GRAYLOG_PASSWORD_SECRET=”%s”\nGRAYLOG_ROOT_PASSWORD_SHA2=”%s”\n’ “$SECRET” “$HASH” > .env

2. Write docker-compose.yml

This is Graylog’s official open-core compose file with three changes for a Wazuh host: Data Node ports 9200/9300 stay internal, the web UI listens on all interfaces, and only the needed inputs are published.

3. Run the installer as root

4. Check the containers

docker compose -f ~/graylog/docker-compose.yml ps

NAME IMAGE STATUS PORTS
graylog-datanode-1 graylog/graylog-datanode:7.1 Up 2 minutes 127.0.0.1:8999->8999/tcp
graylog-graylog-1 graylog/graylog:7.1 Up 2 minutes (unhealthy) 0.0.0.0:9000->9000/tcp, 0.0.0.0:5140->5140/udp, 127.0.0.1:5555->5555/tcp, 0.0.0.0:12201->12201/tcp
graylog-mongodb-1 mongo:7.0 Up 2 minutes 27017/tcp

The unhealthy status is expected at this point: Graylog waits in preflight mode until the Data Node is configured.

First login & preflight

Preflight secures the Data Node with a certificate authority, and then Graylog starts normally. It takes four clicks and about 2 minutes.

Preflight uses a one-time password printed in the Graylog log. Read it with this command, then open http://10.10.32.19:9000 and log in as admin with that password:

docker logs graylog-graylog-1 2>&1 | grep -i ‘initial configuration’
# Initial configuration is accessible at 0.0.0.0:9000, with username ‘admin’ and password ‘<one-time password>’.

1. Create the CA. Enter an organization name (here Example Graylog CA) and click Create CA. The registered Data Node shows as http://172.18.0.2:8999/ — datanode.

Press enter or click to view image in full size

2. Renewal policy. Keep Automatic with a 30 day certificate lifetime and click Create policy.

Press enter or click to view image in full size

3. Provision certificates. Click Provision certificate and continue. The Data Node restarts with TLS on https://datanode:9200.

Press enter or click to view image in full size

4. Resume startup. When all four steps show green checks, click Resume startup. After about a minute, docker compose ps shows Graylog as healthy and the log says Graylog server up and running.

Press enter or click to view image in full size

From now on, log in at http://10.10.32.19:9000 as admin with the password stored in ~/graylog/.admin_password. The preflight password no longer works.

Press enter or click to view image in full size

On first login Graylog asks to collect anonymous usage data. Here it was declined with Cancel; you can change it later in the user profile.

Press enter or click to view image in full size

Confirm the server is up:

docker compose -f ~/graylog/docker-compose.yml ps — format ‘{{.Name}} {{.Status}}’
# graylog-datanode-1 Up 5 minutes
# graylog-graylog-1 Up 5 minutes (healthy)
# graylog-mongodb-1 Up 5 minutes
docker logs graylog-graylog-1 2>&1 | grep ‘up and running’

Create the Wazuh input in Graylog

A Raw/Plaintext TCP input on port 5555 receives one JSON alert per line from Fluent Bit. Graylog 7’s Input Setup Wizard then creates the stream, the index set link, the pipeline and the routing in one pass.

Launch the input

1. Go to System → Inputs. In Select input choose Raw/Plaintext TCP, then click Launch new input.

Press enter or click to view image in full size

2. Fill the form and click Launch Input:

Press enter or click to view image in full size

Create the index set (90-day retention)

A separate index set keeps Wazuh fields out of the default index and gives Wazuh its own retention. Go to System → Indices → Create index set. Pick the 30 Days Hot template, click Apply template, then change the values below and click Create index set.

Press enter or click to view image in full size
Press enter or click to view image in full size

The new index set appears next to the default ones, and the OpenSearch cluster reports green.

Press enter or click to view image in full size

Run the Input Setup Wizard

New inputs start in SETUP mode and receive no traffic until the wizard finishes.

1. On the input row, click Set-up Input, then Create Stream.

Press enter or click to view image in full size

2. Fill in the stream:

o Title Wazuh Alerts, description Wazuh manager alerts shipped by Fluent Bit from alerts.json

o Check Remove matches from ‘Default Stream’, so alerts are stored once

o Check Create a new pipeline for this stream

o Select Index Set → Wazuh Alerts, then click Next

Press enter or click to view image in full size
Press enter or click to view image in full size

3. Click Start Input. The wizard reports each step as it completes.

Press enter or click to view image in full size
Press enter or click to view image in full size

The wizard creates these objects:

Check from the CLI:

P=$(cat ~/graylog/.admin_password)
curl -s -u admin:$P http://127.0.0.1:9000/api/streams | jq -r ‘.streams[] | “\(.title) index_set=\(.index_set_id)”’
curl -s -u admin:$P http://127.0.0.1:9000/api/system/indices/index_sets | jq -r ‘.index_sets[] | “\(.title) \(.index_prefix)”’
ss -tln | grep 5555 # 127.0.0.1:5555 published by docker-proxy

Ship Wazuh alerts with Fluent Bit

Fluent Bit 5.1.3 runs on the Wazuh manager. It tails /var/ossec/logs/alerts/alerts.json and sends each alert to 127.0.0.1:5555. In the first 2 minutes it shipped 1,338 records with 0 errors, 0 retries and 0 drops.

Configuration: /etc/fluent-bit/fluent-bit.conf

Install and start

The installer put this file in place. To apply a change:

/opt/fluent-bit/bin/fluent-bit — dry-run -c /etc/fluent-bit/fluent-bit.conf # validate
sudo install -m 0644 /home/analyst/graylog/fluent-bit.conf /etc/fluent-bit/fluent-bit.conf
sudo systemctl restart fluent-bit
systemctl status fluent-bit — no-pager | head -8

● fluent-bit.service — Fluent Bit
Loaded: loaded (/lib/systemd/system/fluent-bit.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2026–10–08 10:57:38 IST

Fluent Bit runs as root, so it can read alerts.json (owned by wazuh:wazuh, mode 640). By default it starts reading at the end of the file, so only new alerts are sent. To backfill the current day, add Read_from_Head On to [INPUT] once.

Check delivery

curl -s http://127.0.0.1:2020/api/v1/metrics | jq ‘.output’
# {“tcp.0”: {“proc_records”: 1338, “errors”: 0, “retries”: 0, “dropped_records”: 0, …}}

In Graylog, System → Inputs now shows the input as RUNNING with traffic.

Parse JSON with a pipeline rule

One rule turns each raw JSON line into fields such as rule_level, rule_description, agent_name, data_srcip and rule_mitre_id_0. It also sets the message time to the Wazuh event time and the source to the agent name.

Create the rule

1. Go to System → Pipelines → Manage rules → Create pipeline rule.

2. Click Use Source Code Editor, then Confirm.

3. Paste the rule and set the description to Parses Wazuh alerts.json lines (via Fluent Bit) into Graylog fields.

rule “Wazuh — parse alert JSON”
when
has_field(“message”) && starts_with(to_string($message.message), “{“)
then
// explode the Wazuh alert JSON into fields: rule.level -> rule_level, agent.name -> agent_name …
let flat = flatten_json(value: to_string($message.message), array_handler: “flatten”);
set_fields(fields: to_map(flat), clean_fields: true);
// use the agent name as the message source
set_field(“source”, to_string($message.agent_name, “wazuh-manager”));
// use the Wazuh event time (renamed by Fluent Bit) as the message timestamp
set_field(“timestamp”, parse_date(value: to_string($message.wazuh_timestamp), pattern: “yyyy-MM-dd’T’HH:mm:ss.SSSZ”));
// short, readable message line: “[level] description”
set_field(“message”, concat(concat(concat(“[“, to_string($message.rule_level)), “] “), to_string($message.rule_description)));
end

4. Test before saving. Under Rule Simulation → Simple Message, paste a sample alert and click Run rule simulation:

{“wazuh_timestamp”:”2026–10–08T11:02:15.412+0530",”rule”:{“level”:5,”description”:”sshd: authentication failed.”,”id”:”5760",”mitre”:{“id”:[“T1110.001”],”tactic”:[“Credential Access”]},”groups”:[“syslog”,”sshd”,”authentication_failed”]},”agent”:{“id”:”001",”name”:”web-01",”ip”:”10.10.32.50"},”manager”:{“name”:”wazuh-manager”},”id”:”1759901535.123456",”full_log”:”Oct 8 11:02:15 web-01 sshd[2211]: Failed password for invalid user test from 10.10.5.23 port 51522 ssh2",”data”:{“srcip”:”10.10.5.23",”srcport”:”51522",”srcuser”:”test”},”location”:”/var/log/auth.log”}

[image: Rule source editor with the simulation input]

[image: Rule simulation result showing the parsed fields]

The simulation returned these fields, which confirms the rule works:

Field

Simulated value

message

[5] sshd: authentication failed.

source

web-01

timestamp

2026–10–08T05:32:15.412Z (11:02:15 IST)

rule_level / rule_id

5 / 5760

rule_mitre_id_0 / rule_mitre_tactic_0

T1110.001 / Credential Access

agent_name / agent_ip

web-01 / 10.10.32.50

data_srcip / data_srcuser

10.10.5.23 / test

full_log

original sshd line

5. Click Create rule.

[image: Pipeline rules list with the new rule next to the wizard’s routing rule]

Attach the rule to the Wazuh Alerts pipeline

1. Go to System → Pipelines → Manage pipelines and open Wazuh Alerts, the pipeline the wizard created.

2. On Stage 0 click Edit. Under Stage rules select Wazuh — parse alert JSON, then click Update stage.

[image: Edit stage 0 dialog]

The pipeline now runs the rule on every message in the Wazuh Alerts stream.

Get Onix.09’s stories in your inbox

Join Medium for free to get updates from this writer.

[image: Wazuh Alerts pipeline with the rule in Stage 0]

Processing order: the input sends the message to the Default Stream. The Default Routing pipeline moves it to Wazuh Alerts. Then the Wazuh Alerts pipeline parses it, and it is stored in wazuh-alerts_*.

Alerts and dashboard

An event definition raises a High-priority event for every Wazuh alert of level 10 or higher, checked every minute. A dashboard shows volume, severity, top rules, agents and MITRE tactics. The stream and index set already exist from the input wizard.

High-severity alert (event definition)

Go to Alerts → Event Definitions → Create event definition and step through the wizard.

1. Event Details. Title Wazuh — High severity alert (level >= 10), priority High, description Fires for every Wazuh alert with rule.level 10 or higher.

[image: Event Details step]

2. Condition. Set these values. The Filter Preview on the right shows matching alerts straight away.

Field

Value

Condition Type

Filter & Aggregation

Search Query

rule_level:>=10

Streams

Wazuh Alerts

Search within the last

1 minute

Execute search every

1 minute

Create Events if

Filter has results

Event Limit

100 per run

[image: Condition step with the Filter Preview]

3. Fields, Notifications, Share. Click Next through these steps. No SMTP server or webhook is configured yet, so no notification is attached.

4. Summary. Check the values and click Create event definition.

[image: Event definition summary]

[image: Event Definitions list showing the new definition running every minute]

Events appear under Alerts within a minute. The first fired at 11:20:20 IST.

[image: Alerts & Events page with High-priority Wazuh events]

To get notified: go to Alerts → Notifications → Create notification and choose Email (set GRAYLOG_TRANSPORT_EMAIL_* variables in docker-compose.yml first), Slack or HTTP. Then edit the event definition and attach it on the Notifications step.

Dashboard: Wazuh Alerts Overview

1. Go to Dashboards → Create new dashboard.

2. Open the Create (+) sidebar and click Message Count.

[image: Create sidebar with widget types]

3. Open the Fields sidebar. For each field below, click it and choose Show top values: rule_level, rule_description, agent_name, rule_mitre_tactic_0.

[image: Fields sidebar with the Show top values action]

4. Click Save as and enter the title Wazuh Alerts Overview and summary Wazuh alert volume, severity, top rules, agents and MITRE tactics. Then click Create dashboard.

[image: Save dashboard dialog]

The saved dashboard lives at http://10.10.32.19:9000/dashboards/<dashboard-id>.

[image: Wazuh Alerts Overview dashboard]

Widgets default to the last 5 minutes. Use the time selector at the top left (for example 1 day or 7 days) to widen every widget at once. To change one widget for good, open its Edit pencil and set its own time range.

Verification & testing

The pipeline works end to end. In the first hour, 870 of 871 Wazuh alerts arrived fully parsed in the wazuh-alerts_0 index. The one failure is explained under Troubleshooting.

1. Search the Wazuh stream

Go to Streams → Wazuh Alerts (or Search with the stream filter) and pick 1 hour. Every message shows as [level] description, and its source is the agent name.

[image: Search on the Wazuh Alerts stream]

Useful queries:

Goal

Query

High-severity alerts

rule_level:>=10

One agent

agent_name:”web-01"

Failed Windows logons

data_win_system_eventID:4625

Brute force (MITRE)

rule_mitre_id_0:T1110*

FIM changes under /etc

syscheck_path:\/etc*

A source IP

data_srcip:10.10.5.23 OR data_win_eventdata_ipAddress:10.10.5.23

Messages the rule could not parse

_exists_:gl2_processing_error

[image: High-severity query rule_level:>=10]

2. Inspect one alert

Click a message to expand it. It should show Received by Wazuh Alerts (Fluent Bit), Stored in index wazuh-alerts_0, Routed into streams Wazuh Alerts, and the Wazuh fields (agent_*, rule_*, data_*).

[image: Expanded alert with parsed fields]

3. Generate a test alert

File integrity monitoring on /etc runs in realtime on this manager, so adding a file there produces an alert within seconds:

sudo touch /etc/graylog-fim-test # Wazuh rule 554 “File added to the system”
# in Graylog search: syscheck_path:”/etc/graylog-fim-test”
sudo rm /etc/graylog-fim-test # rule 553 “File deleted”

This already happened once by accident: installing the new Fluent Bit config raised a syscheck_integrity_changed alert for /etc/fluent-bit/fluent-bit.conf, which arrived in Graylog.

A fake sshd line written with logger did not produce an alert. This manager does not seem to analyse /var/log/auth.log, so use the FIM test (or a real failed login on an agent) instead.

4. Health checks

# containers
docker compose -f ~/graylog/docker-compose.yml ps — format ‘{{.Name}} {{.Status}}’
# Graylog load-balancer status (ALIVE = OK)
curl -s http://127.0.0.1:9000/api/system/lbstatus
# Fluent Bit delivery counters
curl -s http://127.0.0.1:2020/api/v1/metrics | jq ‘.output’
# messages in the last 10 minutes on the Wazuh stream
P=$(cat ~/graylog/.admin_password)
curl -s -u admin:$P -H ‘Accept: application/json’ \
“http://127.0.0.1:9000/api/search/universal/relative?query=*&range=600&limit=1&filter=streams:<wazuh-stream-id>" | jq .total_results

Check

Result on 2026–10–08

Containers

mongodb, datanode up; graylog healthy

OpenSearch cluster

green, 7 active shards, 0 unassigned

Fluent Bit

1,338 records, 0 errors, 0 retries, 0 dropped

Parsed alerts

870 of 871 (1 partial line at Fluent Bit start)

Event definition

firing; first High event at 11:20:20 IST

Troubleshooting & maintenance

Most problems show up in one of three places: docker logs graylog-graylog-1, journalctl -u fluent-bit or the gl2_processing_error field.

Known issues seen during this setup

Symptom

Cause

Fix

graylog-graylog-1 shows unhealthy right after install

Preflight mode until the Data Node CA is set up

Finish preflight (First login section)

Data Node cannot start: port 9200 in use

Wazuh indexer already listens on 127.0.0.1:9200/9300

Do not publish 9200/9300 for the Data Node (as in this compose file)

UI timestamps 5.5 h behind

Admin user defaults to UTC

GRAYLOG_ROOT_TIMEZONE: “Asia/Kolkata”, then docker compose up -d graylog; other users set it in their profile

One message with gl2_processing_error … parse_date … Invalid format: “”

Fluent Bit started mid-line and sent a fragment wrapped as {“log”: …}

One-off at first start; the DB offset file prevents it on restarts

Save greyed out on a new dashboard

A new dashboard must be saved with Save as first

Use Save as, then Save for later edits

Input shows SETUP and receives nothing

Graylog 7 inputs wait for the setup wizard

Click Set-up Input and finish the wizard

Diagnosis commands

# Graylog / Data Node / MongoDB logs
docker logs — tail 100 graylog-graylog-1
docker logs — tail 100 graylog-datanode-1

# Fluent Bit service and errors
systemctl status fluent-bit — no-pager
journalctl -u fluent-bit — since “15 min ago” — no-pager | grep -iE ‘error|warn’

# Is Wazuh writing alerts?
sudo tail -n 2 /var/ossec/logs/alerts/alerts.json

# Is the input port listening?
ss -tlnp | grep 5555

# Parsing failures in Graylog (search bar)
_exists_:gl2_processing_error

If…

Check

No messages at all

Input RUNNING (not SETUP)? Fluent Bit errors/retries growing? ss -tln | grep 5555

Messages in Default Stream, not Wazuh Alerts

Routing rule in System → Pipelines → Default Routing still present?

Messages arrive but no fields

Rule attached to Stage 0 of the Wazuh Alerts pipeline? Message starts with {?

Wrong times

wazuh_timestamp present? Fluent Bit [FILTER] modify in place?

Routine operations

cd ~/graylog

# stop / start the whole stack
docker compose stop
docker compose start

# upgrade within 7.1 (pull newer patch images, recreate)
docker compose pull && docker compose up -d

# disk used by Graylog volumes
sudo du -sh /var/lib/docker/volumes/graylog_*

# back up configuration (MongoDB) to ~/graylog/backup
mkdir -p backup && docker compose exec -T mongodb mongodump — archive — gzip > backup/mongo-$(date +%F).gz

• Retention: the Wazuh Alerts index set keeps 90–100 days. Change it in System → Indices → Wazuh Alerts → Edit.

• Secrets: ~/graylog/.env and ~/graylog/.admin_password are mode 600. Back them up; losing GRAYLOG_PASSWORD_SECRET invalidates stored credentials.

• Access: the analyst user is now in the docker group, which is effectively root on this host. Restrict TCP 9000 to the SOC network if the host is reachable from elsewhere.

  • Files: ~/graylog/ holds docker-compose.yml, .env, fluent-bit.conf, install.sh and screenshots/.

Thanks 👍

--

--