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-pagersudo 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_timeoutdefault is 900 seconds.- You manage it via:
GET /security/configPUT /security/configDELETE /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:
token = get_cached_token()resp = call_api(token)- if resp is 401:
token = authenticate()resp = call_api(token)again
- 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 certificateunable 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.yamlunderhttps: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 connectConnection 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:
GET /security/config(see earlier) Wazuh Documentation
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/usersPUT /security/users/{user_id}
Example flow (Wazuh doc style, but operationalized):
- List users:
curl -k -X GET "https://localhost:55000/security/users?pretty=true" \ -H "Authorization: Bearer $TOKEN"
- 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-wuipassword, 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:
- Authenticate (get token)
- Query agents
- For each agent:
- collect status fields + last keepalive
- compute staleness threshold (example: > 2x expected heartbeat)
- Report:
- stale agents
- disconnected agents
- never seen / orphaned agents
- 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).