Wazuh API Auth Tokens: How to Authenticate, Debug Failures, and Build Reliable Custom Scanners

If you are building custom scanners, health checks, or cross-tool validation (RMM/MDM/EDR vs Wazuh), you will inevitably hit the same first gate:

  • "How do I authenticate to the Wazuh server API reliably?"
  • "Why does it work in the dashboard but my script gets 401/403/SSL errors?"
  • "Why does it randomly fail after 15 minutes?"
  • "Why does it work locally but not remotely?"

This post is a practical, troubleshooting-heavy walkthrough of Wazuh API authentication and token usage, with real commands and failure-mode diagnostics you can drop into a scanner.

Wazuh requires a JWT token on all server API endpoints, obtained via POST /security/user/authenticate. Wazuh Documentation


What you are authenticating to (be precise)

Wazuh has multiple "APIs" in the ecosystem (server API vs indexer API, etc.). This post is about the Wazuh server API (the one commonly on port 55000), because that is what you use to query agents, groups, manager status, and most operational state.

The Wazuh "Getting started" docs are explicit: all endpoints require a JWT bearer token in each request. Wazuh Documentation


Authentication flow (how it actually works)

Step A: Basic auth to obtain a JWT token

You send a request to:

  • POST /security/user/authenticate

Using HTTP Basic Auth (username:password). Wazuh returns a JWT.

Wazuh even documents the exact one-liner pattern for capturing it:

TOKEN=$(curl -u <WAZUH_API_USER>:<WAZUH_API_PASSWORD> -k -X POST "https://localhost:55000/security/user/authenticate?raw=true")

Note the raw=true query param: this returns just the token string (no JSON wrapper), which is ideal for scripts. Wazuh Documentation

Step B: Use the JWT token for all further calls

Every subsequent request includes:

Authorization: Bearer <TOKEN>

Wazuh docs repeat this throughout their API usage examples. Wazuh Documentation+1


Prerequisites you should validate before you blame auth

1) Confirm the API is listening on the address you think

Wazuh server API host and port live in:

  • /var/ossec/api/configuration/api.yaml

Wazuh defaults are typically host on all interfaces and port 55000, but production installs often restrict host to localhost. Wazuh explicitly documents changing host and port here. Wazuh Documentation

On the Wazuh manager box:

sudo ss -lntp | grep 55000

If you see it bound to 127.0.0.1:55000, your remote scanner will never connect.

2) Confirm service health and logs

Wazuh server API is managed under the manager service. Restarting the manager restarts the API. Wazuh Documentation

sudo systemctl status wazuh-manager --no-pager
sudo tail -n 200 /var/ossec/logs/api.log

If you are debugging auth, API logs often show rate limiting, brute-force blocks, TLS issues, and config parse errors.

3) Expect TLS friction (self-signed certs are common)

Wazuh generates a private key and self-signed cert on first run if not provided, stored under:

  • /var/ossec/api/configuration/ssl/

Wazuh documents this behavior and the config knobs in api.yaml. Wazuh Documentation+1

You can temporarily use curl -k to ignore cert validation during troubleshooting, but for scanners you should install the CA properly (more on that later).


Getting a token: the "works everywhere" command set

Option 1: raw token (best for scripts)

This is the most automation-friendly pattern:

TOKEN="$(curl -sS -u "wazuh:YOUR_PASSWORD" -k -X POST \
"https://wazuh.example.com:55000/security/user/authenticate?raw=true")"

echo "Token length: ${#TOKEN}"

If token length is suspiciously small (like 0), your auth failed or the endpoint did not return what you expected.

Then:

curl -sS -k -H "Authorization: Bearer $TOKEN" \
"https://wazuh.example.com:55000/agents?pretty=true" | head

Wazuh explicitly documents raw=true in their getting started instructions. Wazuh Documentation

Option 2: JSON response + parse (useful for debugging)

Some teams prefer JSON responses while troubleshooting because you can see structured errors.

curl -k -u "wazuh:YOUR_PASSWORD" -X POST \
"https://wazuh.example.com:55000/security/user/authenticate?pretty=true"

If you want to extract the token from JSON:

curl -sS -k -u "wazuh:YOUR_PASSWORD" -X POST \
"https://wazuh.example.com:55000/security/user/authenticate" | jq -r .data.token

(Requires jq.)


Token expiration: why scripts "randomly" fail

By default, Wazuh JWT tokens expire after a fixed number of seconds. Wazuh documents:

  • auth_token_exp_timeout default is 900 seconds.
  • You manage it via:
    • GET /security/config
    • PUT /security/config
    • DELETE /security/config

And they warn that changing security config revokes all JWTs (you must re-auth). Wazuh Documentation+1

Inspect token expiration config

curl -sS -k -H "Authorization: Bearer $TOKEN" \
"https://wazuh.example.com:55000/security/config?pretty=true"

Increase expiration (example: 3600 seconds)

curl -sS -k -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://wazuh.example.com:55000/security/config" \
-d '{"auth_token_exp_timeout": 3600}'

Important operational note: if you change this, all existing tokens are revoked. Your scanners should anticipate that and re-auth. Wazuh Documentation

Scanner pattern you actually want

Do not try to make tokens "never expire" as a crutch. Instead:

  • Cache token in memory (or short-lived file)
  • On any 401, re-auth and retry once
  • Do not hammer auth endpoint (respect rate limits / lockouts)

Pseudo-flow:

  1. token = get_cached_token()
  2. resp = call_api(token)
  3. if resp is 401:
    • token = authenticate()
    • resp = call_api(token) again
  4. if still fails, escalate error

Troubleshooting: what each failure looks like and what it means

A) TLS / certificate failures

Symptoms:

  • curl: (60) SSL certificate problem: self signed certificate
  • unable to get local issuer certificate

Quick confirm:

openssl s_client -connect wazuh.example.com:55000 -showcerts </dev/null

Fix options:

  • In dev: curl -k
  • In production scanners: trust the Wazuh API CA properly
  • In Wazuh: configure HTTPS settings in api.yaml under https: including cert/key paths. Wazuh Documentation

Also confirm the certs exist where Wazuh expects them:

  • /var/ossec/api/configuration/ssl/server.crt
  • /var/ossec/api/configuration/ssl/server.key
    Wazuh documents the path and even the log lines you will see when it auto-generates them. Wazuh Documentation+1

B) Connection refused / timeout

Symptoms:

  • Failed to connect
  • Connection refused
  • hangs until timeout

Most common causes:

  • API bound to localhost only (host config)
  • firewall rules blocking 55000
  • reverse proxy misrouting

Check api.yaml host and port and validate binding with ss -lntp. Wazuh calls out host/port settings explicitly. Wazuh Documentation

C) 401 Unauthorized on authenticate

Symptoms:

  • wrong username/password
  • account lockout (too many attempts)
  • hitting the wrong endpoint (proxy / path)

Wazuh documents login attempt limits and blocking behavior in api.yaml (max_login_attempts, block_time) and recommends configuring them. Wazuh Documentation

If you suspect lockout:

  • Check /var/ossec/logs/api.log
  • Wait for block time or adjust config

D) 401 Unauthorized on normal endpoints (but auth succeeded)

This is almost always:

  • token expired (900s default)
  • token revoked (security config change)
  • you forgot Authorization: Bearer ...

Verify expiration config:

E) 403 Forbidden (auth works, endpoint denied)

This is typically RBAC-related: the user can authenticate but does not have permission to access specific resources/endpoints.

Wazuh RBAC is documented as controlling access to endpoints/resources by user privileges. Wazuh Documentation+1

Practical fix:

  • Use an admin API user while prototyping
  • Then create a least-privilege user/role for the scanner once you know exactly which endpoints it needs

F) "Dashboard works but my script fails"

Most common root causes:

  • Dashboard is running locally and can reach localhost:55000
  • Your script is remote and cannot (host binding / firewall)
  • Dashboard is configured with its own credentials that differ from yours
  • TLS trust differs between environments

When this happens, test from the scanner host with:

curl -vk "https://wazuh.example.com:55000/"

The -v output will show DNS resolution, TLS handshake, and HTTP response headers.


Password and user management (the real-world parts)

Wazuh notes that default users may exist depending on install method (e.g., OVA), and recommends changing default passwords. Wazuh Documentation

They also document changing passwords via API using:

  • GET /security/users
  • PUT /security/users/{user_id}

Example flow (Wazuh doc style, but operationalized):

  1. List users:

curl -k -X GET "https://localhost:55000/security/users?pretty=true" \
-H "Authorization: Bearer $TOKEN"

  1. Update password:

curl -k -X PUT "https://localhost:55000/security/users/<USER_ID>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"password":"NEW_STRONG_PASSWORD"}'

Wazuh notes password complexity requirements (length and character variety). Wazuh Documentation

Operational gotcha:

  • If you change the wazuh-wui password, you must update the dashboard config accordingly. Wazuh explicitly warns about this. Wazuh Documentation

Hardening and "scanner-safe" configuration

If you are going to build custom scanners, you want to reduce the fragility you just learned about.

1) Restrict exposure

Wazuh documents changing host from all interfaces to a restricted bind. Wazuh Documentation

Typical pattern:

  • Bind API to localhost
  • Put it behind a reverse proxy that:
    • terminates TLS with your org cert
    • enforces IP allow-lists
    • applies rate limits

If you must expose 55000 directly:

  • lock down via firewall / security groups
  • do not leave defaults

2) Fix TLS for real (stop using -k)

Wazuh supports custom certs via api.yaml https settings and stores certs in /var/ossec/api/configuration/ssl/. Wazuh Documentation+1

In scanner environments:

  • install your CA bundle
  • validate with:
    • curl --cacert /path/to/ca.pem ...

3) Choose a token expiration that matches your scanner cadence

If your scanner runs every minute, a 15 minute token is fine.
If your scanner runs hourly, you will re-auth nearly every run.

You can increase auth_token_exp_timeout, but build proper re-auth anyway, because tokens are revoked on security config changes. Wazuh Documentation+1

4) Respect login attempt limits and blocks

Wazuh documents configurable login limits in api.yaml. Wazuh Documentation

Scanner implication:

  • do not brute force re-auth loops
  • backoff on repeated failures (exponential backoff is fine)

Building a "real" custom scanner: minimal example logic

Here is a practical checklist for an agent-health scanner using the API:

  1. Authenticate (get token)
  2. Query agents
  3. For each agent:
    • collect status fields + last keepalive
    • compute staleness threshold (example: > 2x expected heartbeat)
  4. Report:
    • stale agents
    • disconnected agents
    • never seen / orphaned agents
  5. Emit metrics and logs that make debugging easy

The key is: treat authentication as a renewable capability, not a one-time step.

Wazuh is already telling you this by design: JWT tokens expire and must be renewed. Wazuh Documentation+1


Where this breaks down across multiple tools (and why Fieldmark exists)

Doing this once for Wazuh is manageable.

Doing this across:

  • Wazuh
  • an EDR/XDR
  • an RMM
  • an MDM
  • DNS agent
  • logging agent

...turns into a pile of:

  • different auth flows (JWT, OAuth2, API keys)
  • different token refresh rules
  • different rate limits and IP blocks
  • different "agent healthy" definitions

That is the exact pattern Fieldmark targets: instead of writing and maintaining one-off scanners per vendor, Fieldmark continuously validates cross-agent coverage and health using source-verified queries, and flags drift and silent failures as a first-class output (not a side script).

Subscribe to Fieldmark XAV

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe