What it does here
Wazuh is the SIEM — the security team's single pane of glass. It ingests events from across the stack, matches them against rules, and raises alerts. In this lab it is what turns a failed login into a detected incident.
How it is deployed
Three containers — indexer (stores events), manager (analyses them against rules) and dashboard (the UI). Deliberately, none of them publishes a port: Caddy fronts the dashboard, everything else stays internal. See Installation §4.8.
How events reach it
A small event forwarder — the second component written for this lab — polls Keycloak's event API and writes each authentication event as JSON to a shared log the Wazuh manager reads. The Golonex Portal writes its authorization decisions to the same place. Two custom rules then do the detection:
| Rule | Level | Fires when |
|---|---|---|
| 100110 | 12 (high) | a login is attempted for a non-existent or disabled account — the post-termination alert |
| 100210 | 10 | the Portal blocks an action because of a segregation-of-duties violation |
Settings in the web console
Sign in at wazuh.iamlab.genbay.co as admin. Wazuh is a large product; four areas matter here.
Security events
The main alert view. Filter by rule level or by rule ID to find the alerts this lab generates — 100110 is the one the capstone produces. Open an alert to see the full event: username, source address, timestamp and the rule that matched.
Rules
Under server or ruleset management you can read the rule definitions, including the two custom Golonex rules above. Reading rule 100110 is instructive — it is only a few lines, and it shows that detection is just an expressed expectation: we said a login for a non-existent account is suspicious, so now it is an alert.
Discover / search
Free-text and field search across stored events. Useful for answering the auditor's question — show me everything that happened to this account — rather than browsing alerts.
Index patterns and time range
Wazuh is time-driven: if a view looks empty, widen the time picker before assuming nothing happened. It is the single most common reason a SIEM appears broken.
Three ways to reach OPA
The same engine, three doors. Which one you use says a lot about what you are doing — demonstrating, operating, or debugging.
| Route | Who uses it | Good for |
|---|---|---|
| PDP console in the Portal | instructor, IT Ops | showing the policy and its decisions to a room |
| OPA's REST API from a container | engineers | asking the engine directly, with no app involved |
| Files and scripts on the host | whoever runs the lab | editing policy, watching decisions, running tests |
1 · From the app — the PDP console
IT Admin › Policy decision point
Sign in to the Portal as a user holding it-admins and open IT Admin › Open PDP console. Two tabs:
- Live decisions — recent authorization decisions with user, action, amount, ALLOW / DENY and the reason OPA gave. Click around the Portal in another tab, then refresh this one.
- Policy — the Rego OPA is actually running, read live from the engine rather than from disk, so the page cannot show you something that is not in force. Read-only by default.
An Edit policy button turns that tab into an editor. Saving needs the policy.edit entitlement, applies to OPA immediately, and is written to disk so it survives a restart.
It is fail-safe. OPA compiles the policy before accepting it. A Rego error is rejected with a message and the running policy is untouched — a bad edit cannot take the lab down.
Worth saying out loud to candidates: editing authorization from a web form is a teaching device, not a production pattern. Real policy lives in version control, is reviewed, tested in CI and shipped as a bundle. This console exists so you can feel a policy change take effect — which is precisely why change control matters.
2 · From a container — OPA's own API
OPA listens on opa:8181 on the internal network. It is not published to the host and not exposed through Caddy: the API has no authentication, so it must never leave the private network. Ask it from any container on ciam-net.
Ask for a decision — no app, no login, no session:
docker exec -i portal python -c "import requests;print(requests.post('http://opa:8181/v1/data/golonex/portal/decision',json={'input':{'user':'x','groups':['app-users','finance-ap-approver'],'action':'payment.approve','amount':50001}}).json())"See what policies are loaded:
docker exec portal python -c "import requests;print([p['id'] for p in requests.get('http://opa:8181/v1/policies').json()['result']])"The endpoints that matter: GET /v1/policies (what is loaded), POST /v1/data/golonex/portal/decision (ask), PUT /v1/policies/<id> (replace — this is what the console uses), and GET /health.
3 · From the host — files, scripts and logs
The policy is a plain file on the server, bind-mounted into the container at /policies:
| Path | What it is |
|---|---|
/opt/ciam-lab/opa/policies/portal.rego | the live policy |
/opt/ciam-lab/opa/policies/portal.rego.orig | pristine copy — the restore point for the live-edit demo |
/opt/ciam-lab/opa/policies/portal_test.rego | the policy's own unit tests |
/opt/ciam-lab/opa-ask.sh | ask OPA a question from the command line |
/opt/ciam-lab/opa-watch.sh | live feed of every decision — put it on a projector |
Ask a question:
/opt/ciam-lab/opa-ask.sh payment.approve app-users,finance-ap-approver 50001Watch decisions live (Ctrl-C to stop):
/opt/ciam-lab/opa-watch.shRun the policy's unit tests:
docker exec opa /opa test /policies -vRaw decision log — every decision, with full input and result:
docker logs --tail 20 opa--watch, so a change to portal.rego on disk takes effect only after
docker restart opa — about four seconds, during which the Portal fails closed and denies
everything. The PDP console sidesteps this: it pushes policy through the API, which applies instantly with no
restart.Every decision is audited
The Portal writes one JSON line per decision, and Wazuh turns the interesting ones into alerts:
| Rule | Level | Fires on |
|---|---|---|
100201 | 4 | any denied action, naming the user and the reason |
100202 | 5 | a payment approved, with the amount |
100203 | 4 | an HR record changed — workforce data feeding IGA |
100204 | 4 | an invoice created |
100205 | 10 | the authorization policy itself was changed |
100210 | 10 | a segregation-of-duties violation blocked at the PDP |
Rule 100205 is deliberately the loudest thing in the lab. Ask a candidate why that is right: changing the policy changes every future decision, so it outranks any single denied action.
Try this
- Run the capstone: terminate Alice, attempt her old credentials a few times, then find alert 100110 here. The gap between the attempt and the alert is seconds.
- Then ask the auditor's question — search her username and reconstruct the whole story from provisioning to termination to attempted misuse.