Guilgo Blog

Notes from my daily work with technology.

If you run Wazuh only as a manager (no Indexer or official Dashboard), you don’t need the full Elastic/OpenSearch stack to get a web UI. You can use Grafana — e.g. the one already running in your Kubernetes cluster — as the “frontend”: it talks to the Wazuh REST API via the JSON Datasource plugin and displays agents, security alerts (SCA, MITRE ATT&CK) and parental control in dashboards. This guide is for homelabs or small environments where you only want to see agents, basic alerts and parental control data without deploying the full Indexer + official Wazuh Dashboard. This post explains the approach, why each decision was made, configuration steps and common issues (JWT, Service selectors, plugin urlPath, AdGuard integration and security alerts panel).

Everything described here runs on a single host, raam: the Wazuh manager, the Kubernetes cluster with Grafana, AdGuard Home and the setup scripts. The dashboard screenshot in the post is from that environment; agent and client names and IPs are anonymized.

Unified dashboard in Grafana (Wazuh + AdGuard) on raam: agent stats, status and DNS queries. Names and IPs anonymized.

Screenshot of the “Wazuh - Agents and System” dashboard with AdGuard Home section as seen in Grafana on raam. Top: total agents, active, never connected, DNS queries and blocked. Bottom: agent table (IDs and status), status donut, DNS section with top blocked/queried domains and top clients.

Why I built it: I wanted to see agents, alerts and parental control from a proper screen, without depending on the phone (JuiceSSH, manager console) every time I check if an agent is up or what AdGuard is blocking. That’s the real problem: a single, lightweight interface in the browser.


Context: why Grafana and not the official Wazuh Dashboard

The official Wazuh Dashboard depends on the Indexer (Elasticsearch/OpenSearch): you have to deploy index, ingestion and a frontend that queries that index. In a homelab or an environment where you only want agents, alerts and parental control without such a heavy stack, that becomes cumbersome.

The Wazuh manager REST API already exposes what you need: agent list, status, statistics, events. The idea is to use Grafana as the single interface: if you already have Grafana in Kubernetes (e.g. for other metrics), you only need a datasource that talks to that API. Grafana doesn’t have a native Wazuh datasource, but the marcusolsson-json-datasource plugin lets you set a base URL, authentication (Basic Auth) and make GET requests with parameters; the JSON response can be mapped to panels with JSONPath. So you avoid deploying the Indexer and official Dashboard. In short: this is Wazuh without Elasticsearch, without Indexer and without the official Dashboard — a lightweight dashboard — using only the Wazuh REST API and Grafana as the frontend. It’s a deliberately minimal setup: fewer components, less memory, less failure surface, in exchange for giving up complex indexing and search.

In my case the Wazuh manager runs in Docker on the same host raam; Grafana runs inside the Kubernetes cluster also on raam (namespace monitoring), image 12.x, internal port 3030. The whole setup — Wazuh, Kubernetes, Grafana, AdGuard and dashboards — is on raam.

If you already use Wazuh for auditing Kubernetes or parental control with AdGuard and Telegram, having the same data in Grafana unifies the view in one place.


Architecture (overview)

  • Browser → NodePort (e.g. 30300) → Grafana (pod in monitoring) with marcusolsson-json-datasource.
  • Grafana datasources: Wazuh API (manager), AdGuard (via a small proxy that normalizes JSON), Wazuh Alerts (proxy that reads alerts.json from the manager and exposes /stats, /alerts).
  • JWT renewal: CronJob (or host cron) every 10 minutes to get a new JWT and update the Wazuh datasource so it doesn’t stop working after 15 minutes.

Main steps (summary)

  1. Expose Grafana via NodePort so you can open it in the browser (e.g. http://<node-IP>:30300).
  2. Install the JSON plugin in Grafana (marcusolsson-json-datasource, tested with 1.3.24). In Kubernetes, use an initContainer or a custom image so the plugin survives pod restarts.
  3. Create the Wazuh API datasource with a fixed UID (so dashboard JSONs keep working). Use JWT or Basic Auth; if JWT, set up the renewal CronJob.
  4. AdGuard integration: The API returns “top” lists as objects with dynamic keys; JSONPath in panels doesn’t handle that well. Deploy a small proxy in the cluster that calls AdGuard, normalizes the JSON to fixed fields (e.g. name, count) and serves it to Grafana.
  5. Security alerts panel: The manager writes alerts to /var/ossec/logs/alerts/alerts.json. Mount that directory from the host into the manager container; run an alerts proxy pod that reads it (read-only) and exposes HTTP endpoints (/health, /stats, /alerts?min_level=7&limit=30). Add a second JSON datasource in Grafana pointing to this proxy and build panels for totals, last hour, last 24h, by severity, by agent, recent alerts table, MITRE tactics.
  6. Run the all-in-one script (once or after reinstalling Grafana) to create the datasource and import dashboards:
    GRAFANA_ADMIN_PASSWORD='...' WAZUH_PASSWORD='...' ./setup-wazuh-grafana-completo.sh

Important gotchas

SymptomCauseSolution
“No data” every ~15 minWazuh JWT expiredUse a CronJob to renew JWT every 10 min and update the datasource; or use Basic Auth. Do not add jwt_expiration_timeout in api.yaml on 4.9.x — the option doesn’t exist and breaks wazuh-apid.
Manager rejects agent: “Incompatible version”Agent version newer than managerInstall the same (or lower) version as the manager, e.g. apt-get install wazuh-agent=4.9.2-1 --allow-downgrades.
Manager doesn’t recognize agent (same host as Docker manager)Connections come from Docker bridge IP (e.g. 172.18.0.1), not 127.0.0.1Register the agent with ip: any.
Panels show no requests / wrong fieldPlugin 1.3.24 uses urlPath in queries, not pathIn dashboard JSON, use urlPath and avoid leading ? in query params (plugin adds it).
Manager unusable after “wazuh-control restart” inside containerDaemons in the container are not supervised by init; they don’t come back after stopUse docker restart <container> to restart; or wazuh-control start (without stop) to only start stopped daemons.
Alerts not visible in Grafanaalerts.json only inside containerMount /var/ossec/logs from host into the manager container; then the alerts proxy can read it via hostPath.

Limitations

This approach does not replace the full Wazuh stack (Indexer + official Dashboard). It’s a light solution for:

  • Basic visualization of agents and status.
  • Simple stats (counts, top N, status).
  • Small environments: homelab, lab, testing.

It is not for complex search over historical events, advanced correlation, long-term indexed retention or forensic analysis on large volumes. There is no indexing: Grafana talks directly to the manager API and, if you add it, to the alerts proxy reading alerts.json. Vulnerability Detection (CVEs) without the Indexer does not persist results; SCA (CIS Benchmark) does write to alerts.json and is a good source for the alerts panel.


For full YAML examples, AdGuard proxy snippet, alerts proxy API details and troubleshooting table, see the Spanish version of this post.

More Wazuh posts on the blog: Auditing Kubernetes with Wazuh and Parental control with Wazuh, AdGuard and Telegram.