Open Source Security IT Platform: Threat Detection, Logging, Alerts, AI and SSO integration.
A real-world implementation with Wazuh, Graylog, MongoDB, Grafana, Nginx, OAuth2-Proxy, Redis, AI an 2026-5-19 09:3:55 Author: infosecwriteups.com(查看原文) 阅读量:13 收藏

Antonio VS

A real-world implementation with Wazuh, Graylog, MongoDB, Grafana, Nginx, OAuth2-Proxy, Redis, AI and SSO — no licenses, no vendor lock-in.

Press enter or click to view image in full size

Managing IT infrastructure security without commercial tools is entirely possible. This documenting the implementation of a complete open source security platform, deployed in production.

This is not a generic tutorial. It’s a guide based on real decisions, and an architecture that runs in production today.

What problem does this platform solve?

In any organization, visibility into what’s happening on servers tends to be fragmented: logs are scattered across different locations, alerts don’t arrive in real time, and each system has its own credentials. The result is an IT team that reacts instead of anticipating.

This platform centralizes three critical capabilities:

  • Threat detection — Wazuh monitors security events, file integrity, and system behavior in real time.
  • Log management — Graylog receives, indexes, alerts, and allows querying all infrastructure logs from a single point.
  • Operational visualization — Grafana delivers real-time dashboards.

All of this is accessible through a single sign-on using the corporate account, thanks to OAuth2-Proxy.

The architecture in brief

The solution is built on five layers:

Detection layer: Wazuh acts as the SIEM/XDR engine. It analyzes events, correlates alerts, and generates notifications. These events/alerts are sent to Graylog via Fluent Bit.

Transport layer: Fluent Bit reads Wazuh events and forwards them to Graylog over RAW TCP. It’s lightweight, efficient, and highly configurable.

Log management layer: Graylog parses the JSON data, indexes all events, enables natural language searches, and exposes an Bridge that we later integrate with Claude via MCP.

Visualization layer: Grafana connects to Wazuh as a datasource and presents data in real-time operational dashboards.

Access layer: Nginx acts as a reverse proxy with TLS ar HTTP header-based authentication. OAuth2-Proxy validates user identity against OIDC and propagates it to each application. Redis stores the sessions.

Technology stack

The entire stack is open source and runs on a single Linux server:

  • Ubuntu 26.04 LTS
  • Wazuh v4.14.5
  • Graylog v7.1.0
  • MongoDB v7.0
  • Grafana v13.0.1
  • OAuth2-Proxy v7.15.2
  • Fluent Bit v5.0.5
  • Nginx v1.28.3
  • Redis v8.0.5

Hardware: Minimum 8 CPU cores, 16 GB RAM
Storage: Dedicated LVM volumes (OS, data and logs separated)

Why this design?

Two architectural decisions deserve explanation:

Separate LVM volumes. The operating system, application data, and logs live on independent partitions. If logs grow out of control, they don’t affect the OS or application data. Scaling log storage is as simple as expanding the corresponding volume.

A single authentication point. Instead of managing users and passwords separately, OAuth2-Proxy delegates all authentication to IdP. The user logs in once and accesses all three systems. Local credentials are eliminated from the lifecycle.

What’s coming in the next articles

This series covers the complete implementation, component by component:

  1. Introduction and architecture ← you are here
  2. Wazuh — SIEM/XDR
  3. MongoDB — Graylog’s DB
  4. Graylog — Log management, data input, alerts
  5. Fluent Bit — Log shipper
  6. Grafana — Real-Time Operational Dashboards
  7. OAuth2-Proxy — Single Sign-On with IdP (Identity Provider)
  8. Redis — Session Persistence and Storage
  9. Nginx — Reverse Proxy with TLS and Header-Based Authentication
  10. Troubleshooting Guide
  11. Authentication — Four steps: SSO and intregation Graylog with Wazuh
  12. Graylog Ingest — Configuring Data Ingest from Fluent Bit
  13. Graylog MCP + Claude — Querying logs in natural language with AI
  14. Final architecture, evidences, and conclusions

Each article includes the exact commands used in production.

Recommendation: Create a working directory. In some sections, we jump to different directories cd; after each step, return to the directory.

Wazuh: SIEM/XDR

Part 2

This article covers the foundation of the entire stack: the server where the platform lives, and the installation and configuration of Wazuh — the SIEM/XDR engine that detects and correlates security events in real time.

The server platform
Before installing any component, it’s worth explaining the storage decision. We use separate LVM volumes for the operating system, application data, and logs:

Volume Size Path Operating system 60 GB / Data 150 GB /data Logs 20 GB /log Swap 4 GB — estimate sizes based on own infrastructure.

Why this separation? If logs grow out of control — and they will — they don’t affect the operating system or application data. Scaling log storage is as simple as expanding the /log volume without touching anything else. The same logic applies to application data in /data.

Wazuh: the detection engine
Wazuh is an open source SIEM (Security Information and Event Management) and XDR (Extended Detection and Response) platform. In practical terms, it continuously monitors system events, correlates alerts, detects file integrity issues, access anomalies, and much more.

In this implementation, Wazuh serves two roles:

  1. Detection — analyzes operating system and service events.
  2. Export — generates alerts in JSON format that Fluent Bit forwards to Graylog.

Installation

Wazuh’s installation is notable for its simplicity: a single script handles the entire stack (Wazuh Manager, Indexer, and Dashboard).

curl -sO https://packages.wazuh.com/4.14/wazuh-install.sh && bash ./wazuh-install.sh -a

Info: Despite the compatibility information, it works without problems with Ubuntu 26.04.

“The recommended systems are: Red Hat Enterprise Linux 7, 8, 9; CentOS 7, 8; Amazon Linux 2; Amazon Linux 2023; Ubuntu 16.04, 18.04, 20.04, 22.04; Rocky Linux 9.4”

Test environments: If the server doesn’t meet the minimum hardware requirements (4 GB RAM, 2 CPU cores), add the -i argument to skip the validation.

Configuration

The Wazuh installer places its data in default paths /var/lib/wazuh-indexer, /var/ossec/logs. We need to mount bind them to our LVM volumes while keeping the original paths functional.

# Stop services

systemctl stop wazuh-indexer wazuh-dashboard wazuh-manager filebeat

Filebeat is included in the Wazuh installation but we’ll replace it with Fluent Bit, which is lighter and more flexible for forwarding events to Graylog.

# Disable Filebeat

systemctl disable filebeat

# Create directory structure

mkdir -p /data/wazuh-indexer/lib /log/wazuh-indexer /data/wazuh/ossec/logs

# Assign permissions to the service user and files

chown wazuh-indexer:wazuh-indexer /data/wazuh-indexer/lib /log/wazuh-indexer
chown wazuh:wazuh /data/wazuh/ossec/logs
chmod 770 /data/wazuh/ossec/logs

# Move existing content

mv /var/lib/wazuh-indexer/* /data/wazuh-indexer/lib/
mv /var/ossec/logs/* /data/wazuh/ossec/logs/

Mount with bind — preserving original paths

The key is using bind mounts: services continue using their default paths, but the actual storage is on the LVM volumes. This avoids modifying Wazuh’s internal configuration.

Evaluate:
With “nofails” the server will start even if the mounts fail, but the services will fail.
Omitting “nofails” will start in emergency mode.

echo "/data/wazuh/ossec/logs /var/ossec/logs none defaults,bind,nofail 0 0" >> /etc/fstab
echo "/data/wazuh-indexer/lib /var/lib/wazuh-indexer none defaults,bind,nofail 0 0" >> /etc/fstab
echo "/log/wazuh-indexer /var/log/wazuh-indexer none defaults,bind,nofail 0 0" >> /etc/fstab
systemctl daemon-reload
mount -a

Compatibility adjustment for Graylog

Wazuh Indexer uses OpenSearch with an option that, by default, forces another version for Filebeat compatibility.

# /etc/wazuh-indexer/opensearch.yml - Comment out

#compatibility.override_main_response_version: true

About the Dashboard warning: When connecting Graylog, the Wazuh Dashboard may display a warning about wazuh-alerts-* index patterns. This is a cosmetic warning that does not affect functionality — it can be safely ignored.

Expected scenario: Filebeat cannot send data to the indexer, and alerts will not appear in the Wazuh dashboard.

warning

Adjusting the Dashboard for Nginx

The Wazuh Dashboard will listen on 127.0.0.1:8080 instead of the default port, since Nginx will act as a reverse proxy with TLS on port 443.

# /etc/wazuh-dashboard/opensearch_dashboards.yml

server.host: 127.0.0.1
server.port: 8080

Credentials and backup
After installation, Wazuh generates a wazuh-install-files.tar file containing certificates, the root CA, and passwords. It's critical to extract and back it up immediately.

tar -xvf wazuh-install-files.tar

The wazuh-passwords.txt file inside contains all automatically generated passwords. Protecting it is mandatory.

MongoDB: Graylog’s DB

Part 3

MongoDB is the database Graylog uses to store its internal configuration: streams, alerts, dashboards, users, and metadata. It does not store the logs themselves — that’s handled by OpenSearch/ElasticSearch — but it’s an essential component for Graylog to function.

An important warning before installing

MongoDB version 8.0 has kernel incompatibilities with Ubuntu 26.04. The stable, tested version for this implementation is 7.0. This is one of those cases where the latest version is not the best choice.

Installation

Add the official MongoDB 7.0 repository and install.

curl -fsSL https://pgp.mongodb.com/server-7.0.asc | gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list
apt update
apt install mongodb-org -y

Configuration

We apply the same pattern as with Wazuh: move data to the /data volume and logs to the /log volume, using bind mounts to keep the original paths intact.

# Create directory structure

mkdir -p /data/mongodb/lib /log/mongodb

# Assign permissions to the service user

chown mongodb:mongodb /data/mongodb/lib /log/mongodb

# Bind mount

echo "/data/mongodb/lib /var/lib/mongodb none defaults,bind,nofail 0 0" >> /etc/fstab
echo "/log/mongodb /var/log/mongodb none defaults,bind,nofail 0 0" >> /etc/fstab
systemctl daemon-reload
mount -a

Why does this pattern repeat?

We apply the same storage strategy to every component in the stack. The reason is simple: in a production environment, data must survive an OS reinstallation. If the OS lives on / (60 GB) and data on /data (150 GB), I can reinstall Ubuntu without losing any application data.

The /log volume (20 GB) is independent because logs have a different lifecycle — they rotate, compress, and get deleted — and we don't want their growth to affect either the OS or application data.

Graylog: Centralized Log Management with TLS Integration to Wazuh Indexer

Part 4

Graylog is the heart of log management in this platform. It receives security events from Fluent Bit, indexes them in OpenSearch (through Wazuh Indexer), enables real-time searches, and exposes an API that we later connect to Claude via MCP.

Installation

Graylog requires Java as a dependency. We install Java 17 and then the Graylog 7.1 server.

apt install openjdk-17-jre-headless -y
wget https://packages.graylog2.org/repo/packages/graylog-7.1-repository_latest.deb
dpkg -i graylog-7.1-repository_latest.deb
apt-get update
apt install graylog-server -y

Configuration

# Create directory structure

mkdir -p /data/graylog/lib/journal /data/graylog/jks /var/lib/graylog-server/journal /log/graylog

# Assign permissions to the service user

chown -R graylog:graylog /data/graylog/lib /var/lib/graylog-server/journal /log/graylog

# Bind mount

echo "/data/graylog/lib/journal /var/lib/graylog-server/journal none defaults,bind,nofail 0 0" >> /etc/fstab
echo "/log/graylog /var/log/graylog-server none defaults,bind,nofail 0 0" >> /etc/fstab
systemctl daemon-reload
mount -a

Credential configuration

Graylog requires two key values in its main configuration file.

# password_secret - internal encryption key (64 characters)

cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 64 | head -n 1

# root_password_sha2 - SHA256 hash of the administrator password

echo -n '<password>' | shasum -a 256 | cut -d' ' -f1

# /etc/graylog/server/server.conf

password_secret = <password_secret>
root_password_sha2 = <root_password>

Integration with Wazuh Indexer
This is one of the most important steps and the one most frequently omitted in generic guides. Graylog needs to connect to Wazuh Indexer (OpenSearch) over HTTPS, which requires it to trust Wazuh’s CA certificate.

The solution is to incorporate Wazuh’s CA into a Java Key Store (JKS) that Graylog can use.

# Copy the base Java keystore

cp /usr/share/graylog-server/jvm/lib/security/cacerts /data/graylog/jks/graylog.jks
cd /data/graylog/jks

# Import the Wazuh CA certificate
# When keytool asks "Trust this certificate? [no]:", answer: y
# Set a custom or random password

keytool -importcert -keystore graylog.jks -storepass <password> -alias wazuh-ca -file /etc/wazuh-indexer/certs/root-ca.pem

Then configure Graylog to use this keystore at startup.

# /etc/default/graylog-server - Enter the password defined in the previous step

GRAYLOG_SERVER_JAVA_OPTS="-Djavax.net.ssl.trustStore=/data/graylog/jks/graylog.jks -Djavax.net.ssl.trustStorePassword=<password>"

# Assign permissions to the service user

chown -R graylog:graylog /data/graylog/jks

Why not just disable TLS verification?

It’s a common temptation to use ssl_verify=false to skip this entire process. The problem is that in production this eliminates a real security layer: any server could present itself as Wazuh Indexer and Graylog would accept it without question. The JKS procedure takes ten extra minutes and guarantees secure communication between components.

Fluent Bit: The Bridge Between Wazuh and Graylog

Part 5

Press enter or click to view image in full size

Fluent Bit is the component that connects Wazuh with Graylog. Its function is simple but critical: read the JSON alerts file that Wazuh generates in real time and forward each event to Graylog over TCP. It’s lightweight, efficient, and consumes minimal resources even under high event load.

Why Fluent Bit instead of Filebeat?

Wazuh installs Filebeat by default to export data to ElasticSearch. Filebeat can be configured to send to Graylog (at the same time, Graylog’s data source is Wazuh), but Fluent Bit is significantly lighter, has better support for complex pipelines, and consumes less memory.

The decision is clear: we disable Filebeat (covered in the Wazuh article) and install Fluent Bit.

Installation

Ubuntu 26.04 (Resolute Raccoon) doesn’t yet have official Fluent Bit packages. The solution is to use the Noble (Ubuntu 24.04) packages, which are compatible.

sh -c 'curl https://packages.fluentbit.io/fluentbit.key | gpg --dearmor > /usr/share/keyrings/fluentbit-keyring.gpg'
echo "deb [signed-by=/usr/share/keyrings/fluentbit-keyring.gpg] https://packages.fluentbit.io/ubuntu/noble noble main" | tee /etc/apt/sources.list.d/fluent-bit.list
apt update
apt install fluent-bit -y

Configuration

# Create directory structure

mkdir -p /log/fluent-bit /var/log/fluent-bit

# Bind mount

echo "/log/fluent-bit /var/log/fluent-bit none defaults,bind,nofail 0 0" >> /etc/fstab
systemctl daemon-reload
mount -a

The main configuration file

This is the core of Fluent Bit. It defines three sections: the global service, the data input (INPUT), and the output (OUTPUT).

Pay attention when copying: The Fluent Bit configuration file is sensitive to indentation. Incorrect indentation will prevent the service from starting.

# /etc/fluent-bit/fluent-bit.conf

[SERVICE]
flush 5
daemon Off
log_level info
log_file /log/fluent-bit/td-agent-bit.log
parsers_file parsers.conf
plugins_file plugins.conf
http_server Off
storage.metrics on
storage.path /tmp/storage
storage.sync normal
storage.checksum off
storage.backlog.mem_limit 5M
[INPUT]
name tail
path /var/ossec/logs/alerts/alerts.json
tag wazuh
parser json
Buffer_Max_Size 5MB
Buffer_Chunk_Size 400k
storage.type filesystem
Mem_Buf_Limit 512MB
[OUTPUT]
Name tcp
Host localhost
Port 5555
net.keepalive off
Match wazuh
Format json_lines
json_date_key true

How the pipeline works

  1. INPUT tail — Reads the /var/ossec/logs/alerts/alerts.json file continuously, similar to tail -f. Every time Wazuh writes a new alert, Fluent Bit detects it.
  2. Tag wazuh — Labels each event so the OUTPUT knows what to process.
  3. OUTPUT tcp — Sends each event to port 5555 on localhost in JSON format, where Graylog will listen with a Raw HTTP input.

The on-disk buffer storage.type filesystem ensures no events are lost if Graylog is momentarily unreachable. Events accumulate and are resent once the connection is re-established.

Optional: REST API — Monitor Fluent Bit data pipelines.
https://docs.fluentbit.io/manual/administration/monitoring

Grafana: Real-Time Operational Dashboards

Part 6

Grafana is the visualization layer of the platform. It connects to Wazuh as a datasource and allows building operational dashboards that show in real time the state of the infrastructure: Wazuh alerts, log volume, access patterns, and any metric Graylog or Wazuh can provide.

In terms of base installation and configuration, Grafana is the simplest component in the stack. The complexity comes later when we integrate SSO authentication.

Installation

wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list
apt update
apt install grafana -y

Configuration

# Create directory structure

mkdir -p /data/grafana/lib /log/grafana

# Assign permissions to the service user

chown grafana:grafana /data/grafana/lib /log/grafana

# Bind mount

echo "/data/grafana/lib /var/lib/grafana none defaults,bind,nofail 0 0" >> /etc/fstab
echo "/log/grafana /var/log/grafana none defaults,bind,nofail 0 0" >> /etc/fstab
systemctl daemon-reload
mount -a

The specific security dashboards — visualizing Wazuh alerts, Graylog logs, and service metrics — depend on the datasources we’ll configure once the entire platform is operational.

A note on the visualization architecture:

In many similar implementations, Wazuh already includes its own dashboard based on OpenSearch Dashboards (Kibana). So why add Grafana?

The reason is datasource flexibility. The Wazuh Dashboard can only visualize data from Wazuh Indexer. Grafana can simultaneously connect to Wazuh, Prometheus, InfluxDB, SQL databases, and dozens of other sources. A single dashboard can show Wazuh alerts alongside infrastructure metrics, application logs, and any other data source — all in real time, with its own alerting system.

OAuth2-Proxy: Single Sign-On with IdP

Part 7

OAuth2-Proxy is the component that eliminates the need to manage users and passwords in Wazuh, Graylog, and Grafana separately. Instead, it delegates all authentication to IdP. The user logs in once with their corporate account and accesses all three systems without entering credentials again.

This article covers the installation of OAuth2-Proxy and its base configuration. The integration with each application is completed in the Authentication article.

How the flow works:

When a user accesses https://site.domain.com

  1. Nginx receives the request and asks OAuth2-Proxy if the user is authenticated auth_request /oauth2/auth
  2. If there’s no active session, OAuth2-Proxy redirects the user to IdP portal.
  3. The user authenticates with their corporate account.
  4. IdP returns an ID Token to OAuth2-Proxy.
  5. OAuth2-Proxy validates the token and creates a session stored in Redis.
  6. Nginx propagates the user’s identity in an HTTP header.
  7. Each application receives the header and assigns permissions accordingly.

OAuth2 Proxy uses several layers to confirm a token’s validity

Signature Verification: The proxy checks that the token’s cryptographic signature was created by the trusted Identity Provider. It typically retrieves public keys automatically from the IdP JWKS (JSON Web Key Set) endpoint, which is discovered via the OpenID Connect well-known configuration URL.

Claim Validation: Once the signature is verified, it inspects specific fields (claims) within the token:

iss (Issuer): Must match the configured provider URL.

aud (Audience): Must contain the client_id of the OAuth2 Proxy instance to ensure the token was intended for this specific application.

exp (Expiration): Ensures the token has not expired.

The result: a single sign-on for the entire platform, with persistent sessions stored in Redis.

Installation

wget https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.2/oauth2-proxy-v7.15.2.linux-amd64.tar.gz
tar -xzvf oauth2-proxy-v7.15.2.linux-amd64.tar.gz
chown root:root oauth2-proxy-v7.15.2.linux-amd64/oauth2-proxy
mv oauth2-proxy-v7.15.2.linux-amd64/oauth2-proxy /usr/sbin/

Configuration

# Create directory structure

mkdir -p /etc/oauth2-proxy /log/oauth2-proxy

# Create service user

useradd -d /dev/null oauth2-proxy -s /usr/sbin/nologin

# Create files

touch /etc/systemd/system/oauth2-proxy.service
touch /etc/oauth2-proxy/service.cfg

# Assign permissions to the service user

chown oauth2-proxy:oauth2-proxy /log/oauth2-proxy

# Generate the cookie secret (32 random characters)

cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 32 | head -n 1

Configuration file:

Case with IdP Microsoft Entra ID.

Enable debugs on errors.

# /etc/oauth2-proxy/service.cfg

client_id = "<app_id>"
client_secret = "<app_secret>"
oidc_issuer_url = "https://login.microsoftonline.com/<tenant_id>/v2.0"
cookie_secret = "<cookie_secret>"
cookie_domains = "<domain.com>"
email_domains = "<domain.com>"
whitelist_domains = "*.<domain.com>"
cookie_expire = "24h"
cookie_httponly = true
cookie_refresh = "50m"
cookie_secure = true
logging_filename = "/log/oauth2-proxy/oauth2.log"
logging_max_size = 100
logging_max_age = 5
provider = "oidc"
provider_display_name = "OIDC"
redis_connection_url = "redis://127.0.0.1:6379"
scope = "openid offline_access"
session_store_type = "redis"
set_xauthrequest = true
silence_ping_logging = true
skip_provider_button = true
upstreams = [ "file:///dev/null" ]
#show_debug_on_error = true

More details: https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview

Registering in Microsoft Entra ID

Important: The Authorization Code Flow is the primary method to authenticate users, an industry-standard. Both tokens — Access and ID — are not enabled for implicit or hybrid flows.

The variety of flows often leads to confusion. With Auth Code Flow and openid scope, we will always obtain an Identification Token👍

For OAuth2-Proxy to work, you need to register an application in the Azure portal:

  1. Azure Portal → Entra ID → App registrations → New registration
  2. Application name, account type, and redirect URI: https://system.<domain.com>/oauth2/callback
  3. Obtain the Application Client ID and Tenant ID.
  4. Create a Client Secret under “Certificates & Secrets”
  5. In “API permissions”, add openid and offline_access

These values are used in client_id, client_secret, and oidc_issuer_url in the configuration file.

Create service

# /etc/systemd/system/oauth2-proxy.service

[Unit]
Description=OAuth2 Proxy Daemon
After=network.target
[Service]
User=oauth2-proxy
Group=oauth2-proxy
Type=simple
ExecStart=/usr/sbin/oauth2-proxy --config=/etc/oauth2-proxy/service.cfg
Restart=always
RestartSec=10
ProtectSystem=full
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target

Redis: Session Persistence and Storage

Part 8

Redis solves a real problem: OAuth2-Proxy session cookies can grow large (cookie bloat) and exceed size limits. Storing sessions in Redis instead of in the cookie keeps the size controlled and allows sessions to survive proxy restarts.

Installation

apt install redis-server -y

Configuration

# Create directory structure

mkdir -p /data/redis/lib /log/redis

# Assign permissions to the service user

chown redis:redis /data/redis/lib /log/redis

# Bind mount

echo "/data/redis/lib /var/lib/redis none defaults,bind,nofail 0 0" >> /etc/fstab
echo "/log/redis /var/log/redis none defaults,bind,nofail 0 0" >> /etc/fstab
systemctl daemon-reload
mount -a

Nginx: Reverse Proxy with TLS and Header-Based Authentication

Part 9

Nginx is the entry point to the entire platform. It acts as a reverse proxy with TLS, routes traffic to Wazuh, Graylog, MCP, and Grafana, and coordinates with OAuth2-Proxy (except with MCP) to validate user identity on every request.

Installation

apt install nginx -y

Configuration

# Create directory structure

mkdir -p /log/nginx /etc/nginx/TLS

# Create files

touch /etc/nginx/sites-available/{wazuh,graylog,graylog-mcp,grafana}
touch /etc/nginx/snippets/{security-headers.conf,oauth2-proxy.conf}
touch /etc/nginx/TLS/{certificate.crt,private.key}

# Delete default host

rm /etc/nginx/sites-enabled/default

# Bind mount

echo "/log/nginx /var/log/nginx none defaults,bind,nofail 0 0" >> /etc/fstab
systemctl daemon-reload
mount -a

Main configuration: nginx.conf

The most notable aspect of this configuration is the map block: it extracts the username from the email returned by OIDC, taking everything before the @. This allows receive the username instead of the full email address.

To avoid excessive logging, the access log is set to off.

# /etc/nginx/nginx.conf

user www-data;
worker_processes auto;
worker_cpu_affinity auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {

worker_connections 1024;

}

http {

map $upstream_http_x_auth_request_email $http_x_auth_user {
"~^(?<user>[^@]+)@" $user;
}

include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 1000;
types_hash_max_size 2048;
server_tokens off;
gzip on;
gzip_vary on;
gzip_min_length 256;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!MD5:!3DES:!CBC:!SHA1';
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 15m;
access_log /var/log/nginx/access.log combined buffer=512k flush=1m;
error_log /var/log/nginx/error.log;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

}

Security Headers snippet

Best practices that strengthen your website’s security against common attacks.

# /etc/nginx/snippets/security-headers.conf

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header X-Download-Options "noopen";
add_header Permissions-Policy 'geolocation=(), microphone=(), camera=()';
add_header Referrer-Policy 'no-referrer';
add_header Content-Security-Policy 'upgrade-insecure-requests';

OAuth2-Proxy snippet

This snippet is included in every virtual host that requires authentication. It defines two locations: one for the OAuth2 flow and one internal for validating sessions.

# /etc/nginx/snippets/oauth2-proxy.conf

location /oauth2/ {
proxy_pass http://localhost:4180;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
access_log off;
}

location = /oauth2/auth {
internal;
proxy_pass http://localhost:4180;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}

The Content-Length: "" headers and proxy_pass_request_body off are critical. Without them, authentication requests to internal APIs fail. This configuration has been validated in production.

Virtual hosts: one per application

Each application has its own configuration file. The pattern is consistent:

  • Redirect HTTP to HTTPS (301).
  • TLS with Wildcard certificate.
  • Include security headers and OAuth2-Proxy snippet.
  • Proxy pass to each application’s local port.

Wazuh Host

Firstly, proxy authentication in Wazuh does not require creating internal users.

This configuration is intended for administration; therefore, it has the "admin" backend role hardcoded.

proxy_set_header x-proxy-roles "admin"

You can associate Security Groups or Nginx mappings with custom backend roles. See the Custom Backend Roles references later in the authentication section.

# /etc/nginx/sites-available/wazuh

server {
listen 80;
server_name wazuh.<domain.com>;
access_log off;
log_not_found off;
return 301 https://wazuh.<domain.com>$request_uri;
}

server {
listen 443 ssl;
server_name wazuh.<domain.com>;
ssl_certificate /etc/nginx/TLS/certificate.crt;
ssl_certificate_key /etc/nginx/TLS/private.key;
include /etc/nginx/snippets/security-headers.conf;
include /etc/nginx/snippets/oauth2-proxy.conf;
location / {
#auth_request /oauth2/auth;
error_page 401 = /oauth2/start;
auth_request_set $user $http_x_auth_user;
proxy_set_header x-proxy-user $user;
proxy_set_header x-proxy-roles "admin";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_read_timeout 10s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass https://localhost:8080;
access_log off;
log_not_found off;
}
}

Graylog Host

# /etc/nginx/sites-available/graylog

server {
listen 80;
server_name graylog.<domain.com>;
access_log off;
log_not_found off;
return 301 https://graylog.<domain.com>$request_uri;
}

server {
listen 443 ssl;
server_name graylog.<domain.com>;
ssl_certificate /etc/nginx/TLS/certificate.crt;
ssl_certificate_key /etc/nginx/TLS/private.key;
include /etc/nginx/snippets/security-headers.conf;
include /etc/nginx/snippets/oauth2-proxy.conf;
location / {
#auth_request /oauth2/auth;
error_page 401 = /oauth2/start;
auth_request_set $user $http_x_auth_user;
proxy_set_header x-proxy-user $user;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_read_timeout 10s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:9000;
proxy_set_header X-Graylog-Server-URL https://$server_name;
access_log off;
log_not_found off;
}
}

Graylog MCP Host

# /etc/nginx/sites-available/graylog-mcp

server {
listen 443 ssl;
server_name graylog-mcp.<domain.com>;
ssl_certificate /etc/nginx/TLS/certificate.crt;
ssl_certificate_key /etc/nginx/TLS/private.key;
include /etc/nginx/snippets/security-headers.conf;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_read_timeout 10s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:9000/api/;
access_log off;
log_not_found off;
}
}

Grafana Host

# /etc/nginx/sites-available/grafana

server {
listen 80;
server_name grafana.<domain.com>;
access_log off;
log_not_found off;
return 301 https://grafana.<domain.com>$request_uri;
}

server {
listen 443 ssl;
server_name grafana.<domain.com>;
ssl_certificate /etc/nginx/TLS/certificate.crt;
ssl_certificate_key /etc/nginx/TLS/private.key;
include /etc/nginx/snippets/security-headers.conf;
include /etc/nginx/snippets/oauth2-proxy.conf;
location / {
#auth_request /oauth2/auth;
error_page 401 = /oauth2/start;
auth_request_set $user $http_x_auth_user;
proxy_set_header x-proxy-user $user;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_read_timeout 10s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:3000;
access_log off;
log_not_found off;
}
}

The auth_request is intentionally commented during initial configuration — it's enabled in the Authentication article, once all roles and users are configured in each application.

graylog-mcp: a special virtual host without OAuth2 so Claude can access the Graylog MCP Bridge directly with Basic authentication. Covered in the Graylog MCP article.

# Create links

cd /etc/nginx/sites-enabled
ln -s ../sites-available/wazuh
ln -s ../sites-available/graylog
ln -s ../sites-available/graylog-mcp
ln -s ../sites-available/grafana

Configure certificates

/etc/nginx/TLS/certificate.crt — Site and intermediate certificates.
/etc/nginx/TLS/private.key — Private key.

Get Antonio VS’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

TLS and verification

# Certificate with restrictive permissions

chmod 400 /etc/nginx/TLS/*

# Verify configuration before starting

nginx -t

Expected response:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# Restart

systemctl restart nginx

Troubleshooting Guide

Log Paths and Common Issues in a Complex Security Stack

Part 10

When something fails in a stack of this complexity — and something always does — knowing exactly where to look for information is the difference between resolving the problem in five minutes or an hour. This article documents the log paths for all components and the most common problems encountered during implementation.

Real-time log monitoring

To monitor any service in real time.

# Filtered error warn
journalctl -u <service> | grep -i -E "error|warn"

# Limit the number of the last events shown
journalctl -u <service> -n <number>

# Show only the most recent journal entries, and continuously print new entries
journalctl -fu <service>

# Jump to the end and augment log lines with explanation texts from the message catalog
journalctl -xeu <service>

Where <service> can be any of the following:

  • wazuh-dashboard
  • wazuh-indexer
  • wazuh-manager
  • graylog-server
  • mongod
  • grafana-server
  • oauth2-proxy
  • fluent-bit
  • nginx
  • redis-server

Log file paths
For direct access to log files:

tail -f <path>
  • Wazuh-Indexer /log/wazuh-indexer/wazuh-cluster.log
  • Graylog /log/graylog/server.log
  • MongoDB /log/mongodb/mongod.log
  • Grafana /log/grafana/grafana.log
  • Fluent Bit /log/fluent-bit/td-agent-bit.log
  • OAuth2-Proxy /log/oauth2-proxy/oauth2.log
  • Nginx /log/nginx/error.log
  • Redis /log/redis/redis-server.log

Known issue: unexpected server restart

Symptom: Wazuh Manager fails to start after an unexpected server restart. The log shows:

ERROR: Another instance is locking this process. If you are sure that
no other instance is running, please remove
/var/ossec/var/start-script-lock/

Cause: Wazuh creates a lock directory when starting and removes it on clean shutdown. If the server restarts abruptly (power cut, OOM killer, etc.), the directory remains and the next startup fails.

Solution: Remove the lock directory manually and start service.

rm -rf /var/ossec/var/start-script-lock/
systemctl start wazuh-manager

Recommended diagnostic approach

When something isn’t working, the recommended review order is:

  1. Is the service running? systemctl status <service>
  2. Any recent errors? journalctl -xeu <service> --since "10 minutes ago"
  3. Does the service’s own log file have more detail? tail -50 <log_path>
  4. For network issues between components: verify ports are listening
    ss -tlnp | grep <port>
  5. For authentication issues: check the OAuth2-Proxy log and the headers Nginx is sending.

The most frequent problems in this implementation were:

  • Incorrect indentation in fluent-bit.conf (service starts without errors but doesn't process data).
  • Wazuh TLS certificate not included in Graylog’s JKS (connection silently rejected).
  • Service startup order: MongoDB must be running before Graylog.

Authentication: SSO and Integration Graylog with Wazuh

Part 11

Press enter or click to view image in full size

This is the most complex article in the series. Up to this point, all components are installed, but each has its own authentication system. The goal of this article is to configure the three solutions to accept the user identity propagated by OAuth2-Proxy via Nginx — completely eliminating local passwords from the daily login cycle.

Important: Follow the order A->B->C->D. Configuring proxy authentication on a system before the user has been created will result in loss of access.

Verify the response of each curl command before proceeding.

# Create random passwords for users

cat /dev/urandom | tr -dc "a-zA-Z0-9" | fold -w 16 | head -n 1

How Proxy Authentication Works

The mechanism is as follows: Nginx validates the user’s identity with OAuth2-Proxy and then injects the username as an HTTP header x-proxy-user in each request to applications. Each application must be configured to trust this header and automatically assign roles.

Continue with the following four steps:

11-A. Wazuh OAuth2

# Reload systemctl and start services

systemctl daemon-reload
systemctl start oauth2-proxy wazuh-manager wazuh-indexer wazuh-dashboard

Startups may take some time. Check the logs for errors before continuing.

Creating Role Mapping

Wazuh needs a role mapping that associates the username propagated by the proxy with the administrator role.

Option A — Web
https://wazuh.<domain.com>/app/security#/security?tab=roleMapping

Use the “admin” credential from the “wazuh-passwords.txt” file.

  • Role mapping name: ProxyAuth
  • Roles: administrator
  • Custom rules → “Add new rule”
  • User field: user_name
  • Search operation: MATCH
  • Value: <AD_user>

Option B — API

Use the “wazuh-wui” credential from the “wazuh-passwords.txt” file.

# Generate token

token=$(curl -u wazuh-wui:<password> -k -X POST "https://localhost:55000/security/user/authenticate?raw=true")

# Create rule

curl -k -X POST "https://localhost:55000/security/rules" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d '{
"name": "ProxyAuth",
"rule": {
"MATCH": {
"user_name": "<AD_user>"
}
}
}'

Rule created with ID: 100

# Assign the rule to the administrator role

curl -k -X POST "https://localhost:55000/security/roles/1/rules?rule_ids=<id_rule>" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json"

— — — end options

Configure proxy authentication in OpenSearch

# /etc/wazuh-dashboard/opensearch_dashboards.yml - Add and replace (part of the file)

opensearch_security.auth.type: "proxy"
opensearch_security.proxycache.user_header: "x-proxy-user"
opensearch_security.proxycache.roles_header: "x-proxy-roles"
opensearch_security.proxycache.proxy_header: "x-forwarded-for"
opensearch_security.proxycache.proxy_header_ip: "127.0.0.1"
opensearch.requestHeadersAllowlist: ["securitytenant","Authorization","x-proxy-user","x-proxy-roles","x-forwarded-for"]

# /etc/wazuh-indexer/opensearch-security/config.yml — Enable xff and proxy auth (part of the file)

xff:
enabled: true
internalProxies: '127\.0\.0\.1'

proxy_auth_domain:
description: "Authenticate via proxy"
http_enabled: true

# /etc/nginx/sites-enabled/wazuh - Enable

auth_request /oauth2/auth;

# Apply changes

cd /etc/wazuh-indexer/opensearch-security/
/usr/share/wazuh-indexer/plugins/opensearch-security/tools/securityadmin.sh \
-cacert /etc/wazuh-indexer/certs/root-ca.pem \
-cert /etc/wazuh-indexer/certs/admin.pem \
-key /etc/wazuh-indexer/certs/admin-key.pem \
-h 127.0.0.1
systemctl restart wazuh-dashboard nginx

As of now, Wazuh uses OAuth2-Proxy for authentication.

Custom Backend Roles

If your goal is to replace the backend role hardcoded with a Group 365:

  • In the App Entra ID, add Security Group ID in Token optional Group Claims (Token configuration).
  • Next, obtain the Object ID of group 365.
  • Duplicate the “all_access” role at: https://wazuh.<domain.com>/app/security-dashboards-plugin#/roles/duplicate/all_access and name it "Group365" for reference.
  • Then, go to the mapping page: https://wazuh.<domain.com>/app/security-dashboards-plugin#/roles/edit/Group365/mapuser and paste the Object ID into the Backend roles field.
  • In /etc/wazuh-indexer/opensearch.yml, add "Group365" to the list defined under plugins.security.restapi.roles_enabled.
  • In Nginx Wazuh Host set auth_request_set $roles $upstream_http_x_auth_request_groups and proxy_set_header x-proxy-roles $roles.
  • Restart wazuh-indexer and nginx systemctl restart wazuh-indexer nginx

References

# /etc/nginx/sites-enabled/wazuh
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/start;
auth_request_set $user $http_x_auth_user;
proxy_set_header x-proxy-user $user;
auth_request_set $roles $upstream_http_x_auth_request_groups;
proxy_set_header x-proxy-roles $roles;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_read_timeout 10s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass https://localhost:8080;
access_log off;
log_not_found off;
}
# /etc/wazuh-indexer/opensearch.yml
plugins.security.restapi.roles_enabled:
- "all_access"
- "security_rest_api_access"
- "Group365"

Alernative Map Nginx

# /etc/nginx/nginx.conf
http {
map $upstream_http_x_auth_request_email $http_x_auth_user {
"~^(?<user>[^@]+)@" $user;
}
map $http_x_auth_user $roles {
"<AD_user1>" "<group_object_id>";
"<AD_user2>" "readall";
}

11-B. Connect Graylog to Wazuh Indexer

Create user in Wazuh for Graylog

Graylog needs a user in Wazuh Indexer to be able to index logs.

Use the “admin” credential from the “wazuh-passwords.txt” file.

User defined “graylog” (or you can choose another name).

Option A — Web
https://wazuh.<domain.com>/app/security-dashboards-plugin#/users

  • Username: graylog
  • Password: <password>
  • Backend roles: admin

Option B — API

# Create user

curl -k -X PUT "https://127.0.0.1:9200/_plugins/_security/api/internalusers/graylog" \
-H "Content-type: application/json" \
-u "admin:<password>" \
-d '{
"password": "<password>",
"backend_roles": ["admin"]
}'

— — — end options

# /etc/graylog/server/server.conf - Enable ElasticSearch connection

elasticsearch_hosts = https://graylog:<password>@127.0.0.1:9200

Use “127.0.0.1” and not “localhost” — Wazuh’s TLS certificate requires the SAN (Subject Alternative Name) to match exactly.

11-C. Graylog OAuth2

# Start services

systemctl start mongod graylog-server

Use the “admin” credential, reference.

Option A — Web

Enable header
https://graylog.<domain.com>/system/authentication/authenticator/edit

  • Enabled: ✓
  • Username header: x-proxy-user

Create user
https://graylog.<domain.com>/system/users/new

  • First Name: <first_name>
  • Last Name: <last_name>
  • Username: <AD_user>
  • E-Mail Address: <email>
  • Assign Roles: Admin
  • Password: <password>

Option B — API

# Enable header

curl -X PUT "http://localhost:9000/api/system/authentication/http-header-auth-config" \
-H "Content-Type: application/json" \
-H "X-Requested-By: cli" \
-u "admin:<password>" \
-d '{
"enabled": true,
"username_header": "x-proxy-user"
}'

# Create user

curl -X POST "http://localhost:9000/api/users" \
-H "Content-Type: application/json" \
-H "X-Requested-By: cli" \
-u "admin:<password>" \
-d '{
"first_name": "<first_name>",
"last_name": "<last_name>",
"username": "<AD_user>",
"email": "<email>",
"password": "<password>",
"roles": ["Admin"],
"permissions": []
}'

— — — end options

# /etc/graylog/server/server.conf -Enable and edit

trusted_proxies = 127.0.0.1/32

# /etc/nginx/sites-enabled/graylog - Enable

auth_request /oauth2/auth;

# Apply changes

systemctl restart graylog-server nginx

11-D. Grafana OAuth2

# Start service

systemctl start grafana-server

Create user

Use the default “admin:admin” credential.

Option A — Web
https://grafana.<domain.com>/admin/users/create

  • Name: <name>
  • Email: <email>
  • Username: <AD_user>
  • Password: <password>

Next screen:

  • Enable "Grafana Admin” and change to "Admin" role.

Option B — API

# Create user

curl -X POST "http://localhost:3000/api/admin/users" \
-H "Content-Type: application/json" \
-u "admin:<password>" \
-d ' {
"name":"<name>",
"email":"<email>",
"login":"<AD_user>",
"password":"<password>"
}'

Account created with ID: 2

# Assign global admin

curl -X PUT "http://localhost:3000/api/admin/users/<user_id>/permissions" \
-H "Content-Type: application/json" \
-u "admin:<password>" \
-d '{
"isGrafanaAdmin": true
}'

# Assign organization administrator role

curl -X PATCH "http://localhost:3000/api/orgs/1/users/<user_id>" \
-H "Content-Type: application/json" \
-u "admin:<password>" \
-d '{
"role":"Admin"
}'

— — — end options

# /etc/grafana/grafana.ini - Enable and edit

[auth.proxy]
enabled = true
header_name = x-proxy-user
header_property = username
auto_sign_up = false
sync_ttl = 3600
whitelist = 127.0.0.1

# /etc/nginx/sites-enabled/grafana - Enable

auth_request /oauth2/auth;

# Apply changes

systemctl restart grafana-server nginx

Important: For security reasons, replace the default password for the user “admin”.

Graylog Ingest: Configuring Data Ingest from Fluent Bit

Part 12

With all components installed and SSO authentication configured, it’s time to connect the final wires: configure Graylog to receive the events that Fluent Bit sends from Wazuh, and activate all services so that the platform is fully operational.

Fluent Bit sends Wazuh events to “localhost:5555” in JSON format. Graylog needs an active input listening on that port to process them.

Option A — Web interface:
https://graylog.<domain.com>/system/inputs

  • Select input type: Raw HTTP
  • Click “Launch new input”
  • Configure:
    Title: Wazuh
    Bind address: 127.0.0.1
  • Click “Launch Input”
  • Follow the wizard to start the input and verify your diagnosis.

Option B — API:

# Create RAW HTTP input - Graylog admin credential

curl -X POST "http://localhost:9000/api/system/inputs" \
-H "Content-Type: application/json" \
-H "X-Requested-By: cli" \
-u "admin:<password>" \
-d '{
"title": "Wazuh",
"type": "org.graylog2.inputs.raw.http.RawHttpInput",
"global": true,
"configuration": {
"bind_address": "127.0.0.1",
"port": 5555,
"recv_buffer_size": 1048576,
"max_chunk_size": 65536
}
}'

bind_address: 127.0.0.1 limits listening to the local interface — Fluent Bit runs on the same server, so there’s no need to expose the port externally.

Standard configuration uses the default stream.

— — — end options

With the platform fully configured, we enable automatic service startup and start Fluent Bit to begin receiving events:

systemctl enable fluent-bit mongod graylog-server grafana-server oauth2-proxy
systemctl start fluent-bit

Order matters: MongoDB must be running before Graylog, and OAuth2-Proxy before Nginx. The “enable” command ensures that systemd respects dependencies on future server restarts.

Verification: Is everything Working?

Once all services are active, verify the complete flow:

  1. Fluent Bit is reading Wazuh alerts:
    tail -f /log/fluent-bit/td-agent-bit.log
    You should see error-free processing lines.
  2. Graylog is receiving messages: In the web interface, navigate to Search and verify that messages with the source “wazuh” are arriving.
  3. SSO Authentication: Access https://site.domain.com from your browser — it should redirect you to the IdP and then return authenticated.
  4. Grafana connected to Wazuh: In Grafana, configure a data source pointing to Wazuh and verify that it returns data.

Graylog MCP + Claude: Querying Security Logs in Natural Language with AI

Part 13

Press enter or click to view image in full size

This is the most groundbreaking article in the series. The platform is already operational: Wazuh detects threats, Fluent Bit transports events, Graylog indexes them, and Grafana visualizes them. But there’s an additional layer that completely changes how we interact with security data: integrating Claude as an AI client on the Graylog REST API — MCP is more than an API; it’s a Bridge.

What is MCP and why does it matter?
MCP (Model Context Protocol) is a protocol that allows language models like Claude to connect directly with external tools and data sources. In this case, Graylog exposes its API as an MCP server, and Claude acts as an intelligent client that can query, filter, and analyze security logs.

The change this produces is significant:

  • Instead of building queries in the Graylog interface, the analyst writes in natural language: “Were there any failed login attempts in the last 2 hours?
  • Instead of reviewing hundreds of log lines, Claude summarizes the relevant patterns and presents them in context.
  • Non-technical users — operators without SIEM experience — can interact directly with the security data.
  • Every query is logged in Graylog like any other API interaction, maintaining complete traceability.

Architecture of integration

A specific virtual host was created in Nginx “graylog-mcp” without OAuth2-Proxy — Claude authenticates directly with Basic credentials encoded in base64.

Installation

On the client:

  1. NodeJS: https://nodejs.org/en/download
  2. Claude Desktop: https://claude.com/download

Configuration in Graylog

Step 1: Enable MCP in Graylog

https://graylog.<domain.com>/system/configurations/MCP

Step 2: Create a user token

Navigate to https://graylog.<domain.com>/system/users → Actions → More → Edit tokens.

  • Token Name: <name>
  • Token TTL: <time>

TTL Syntax Examples: for 60 seconds: PT60S, for 60 minutes: PT60M, for 24 hours: PT24H, for 30 days: P30D

Step 3: Base64 Encoding of Credentials

The format is "<token>:token” encoded in base64.

echo -n "<token>:token" | base64

Claude Desktop Configuration

Edit the “claude_desktop_config.json” file in Claude Desktop, usually located in "%APPDATA%\CLAUDE\".

{
"mcpServers": {
"Graylog": {
"command": "C:\\Program Files\\nodejs\\npx",
"args": [
"mcp-remote",
"https://graylog-mcp.<domain.com>/mcp",
"--header",
"Authorization: Basic <credential_b64>"
]
}
}
}

Replace <domain.com> with your actual domain and <credential_b64> with the base64 value generated in the previous step.

Restart Claude Desktop and check the connection status in:
Settings → Developer

The state should be running or refer to the logs.

Press enter or click to view image in full size

Real-world use cases

Once connected, Claude can answer questions such as:

  • “How many critical alerts did Wazuh generate today?”
  • “Are there any IPs that repeatedly attempted to connect without success?”
  • “Summarize the events of the last 30 minutes”
  • “Which services experienced errors in the last 6 hours?”

The response is not a list of raw logs — it’s a natural language analysis with relevant patterns identified and contextualized.

Security considerations

  • Access to graylog-mcp is restricted to HTTPS with a valid certificate.
  • Base64-encoded credentials are NOT encrypted — they are only encoded. True security lies in TLS and ensuring the endpoint is only accessible from the corporate network.
  • Every request from Claude is logged in the Graylog access log, maintaining a complete audit trail.
  • Rotating the token periodically is a best practice.

Final Architecture, evidences, and Conclusions: An Open Source Security Platform in Production

Part 14

This is the final article in the series. After thirteen articles covering each component of the stack — from Wazuh to integration with Claude via MCP — it’s time to see the big picture and reflect on what worked, and what cost more than expected.

Wazuh

Press enter or click to view image in full size

Press enter or click to view image in full size

Graylog

Press enter or click to view image in full size

Grafana

Press enter or click to view image in full size

Claude

Press enter or click to view image in full size

Press enter or click to view image in full size

Press enter or click to view image in full size

The Complete Architecture

Press enter or click to view image in full size

The diagram shows two main flows that coexist on the platform:

Data Flow

Wazuh detects events and writes them in JSON format. Fluent Bit continuously reads this file and forwards each event to Graylog via TCP. Graylog indexes the data to Wazuh Indexer (returns the connection to OpenSearch). Grafana connects to Wazuh as a data source for dashboards. Claude accesses the Graylog MCP Bridge for natural language queries.

Authentication Flow

The user accesses any of the three systems with their corporate account. Nginx queries OAuth2-Proxy on each request. OAuth2-Proxy validates the session (stored in Redis) or redirects to IdP for authentication. Once authenticated, Nginx propagates the username as an HTTP header to the corresponding application.

Lessons Learned

What worked well:

  • Separate LVM volumes — the best implementation decision. On three occasions, logs grew larger than expected; none of these affected the operating system or application data.
  • Single authentication point — after the initial (and complex) setup, the user experience is seamless. One login for three systems.
  • Fluent Bit vs. Filebeat — Fluent Bit’s resource consumption is significantly lower. On a shared server, this matters.
  • Graylog MCP + Claude — the most differentiating component. It completely changed how non-technical users interact with security data.

What cost more than expected:

  • TLS between Graylog and Wazuh Indexer — the JKS and CA certificate import is the step with the most incorrect documentation online. The guide in this article reflects what actually works.
  • Order of operations in Authentication — configuring the authentication proxy before creating users results in loss of access. The order matters.

Is it worth it?

For an organization that wants true visibility into its infrastructure without paying for commercial SIEM tool licenses (which can cost tens of thousands of dollars annually), the answer is yes.

The real cost is implementation time and technical expertise. This well-documented stack can be replicated in a single workday using this guide. The necessary technical knowledge includes Linux administration, basic TLS concepts, and reading the official documentation.

What you get in return is a security platform with features comparable to commercial solutions, complete control over your data, no vendor lock-in, and the ability to extend it with any component you need.

About this series

This implementation was carried out in production for the IT Infrastructure. All documentation reflects real decisions, real problems, and solutions that work in production.

If you have questions, found a bug, or want to share an improvement, the comments are open.

Your Turn — Operational Ownership

From here, the real value comes from how you adapt it to your environment. As the operator or administrator, the next layer is yours to build:

Graylog: Create dedicated indexes and streams per data source; build a JSON extractor for the message field to enable structured search.

Vulnerability visibility: A custom script can reindex Wazuh’s vulnerability summary index and inject it into Graylog via a new GELF HTTP input — all through the APIs — making vulnerability summaries available in Grafana dashboards .

Alerting and reporting: Define alert conditions and scheduled reports based on what matters to your organization.

Grafana dashboards: Design views tailored to what your team or clients actually need to see.

API automation: Build scripts to automate recurring calls across the stack.

Threat intelligence: Cross-reference events with NIST NVD and CISA KEV for deeper context and custom correlations.

If you have questions or need guidance on the operational side, feel free to reach out — happy to support within my availability.

Author: Antonio Valenzuela Serra SysAdmin Chile May 2026\


文章来源: https://infosecwriteups.com/open-source-security-platform-for-it-infrastructure-centralizing-threat-detection-logs-and-sso-c08d1b412f37?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh