What it does here
Keycloak is the identity provider. It authenticates people, enforces MFA, and issues the OIDC tokens the Golonex Portal trusts. It does not own identities — it federates them from the directory, so the accounts midPoint provisions are exactly the accounts that can sign in.
How it is deployed
Container keycloak with a PostgreSQL database, started in production mode behind Caddy (KC_PROXY_HEADERS=xforwarded, KC_HOSTNAME=https://keycloak.iamlab.genbay.co). Realm, federation and client were created with kcadm.sh — see Installation §4.3.
Settings in the web console
Sign in at keycloak.iamlab.genbay.co/admin/ as admin. You land in the master realm — use the realm selector at the top of the left menu to switch to golonex, which is where everything below lives.
Realm settings
What is configured for the golonex realm:
| Setting | Value here | Why |
|---|---|---|
| Display name | Golonex | what users see on the login page |
| Require SSL | external | HTTPS enforced for outside requests |
| User registration | off | identities come from HR, never self-service |
| Brute force detection | on | locks an account after repeated failures |
| User + admin events | on | the audit trail the SIEM consumes |
Events are worth a visit in their own right: Realm settings › Sessions / Security defenses hold the lockout policy, and the recorded events appear under Realm settings › Events.
User federation › golonex-ldap
The most important screen in Keycloak for this lab. It defines how Keycloak reads the directory:
| Field | Value here | What it means |
|---|---|---|
| Connection URL | ldap://openldap:389 | the directory, on the internal network |
| Bind DN | cn=readonly,dc=golonex,dc=local | a read-only account — Keycloak never writes |
| Users DN | ou=people,dc=golonex,dc=local | where to look for accounts |
| Username LDAP attribute | uid | what becomes the Keycloak username |
| Edit mode | READ_ONLY | governance belongs to midPoint, not here |
| Custom user LDAP filter | (!(employeeType=disabled)) | disabled accounts disappear — this is what makes off-boarding instant |
| Periodic full sync | every 300 s | keeps the imported copy fresh |
Scroll to Mappers on the same provider to see how directory attributes become token claims — first name, title, employeeNumber, and the group mapper that turns ou=groups membership into the groups claim.
The exam point: that one filter line is the whole leaver story. Disable in IGA → the attribute changes in the directory → Keycloak stops seeing the user → every application that trusts Keycloak refuses them.
Clients › golonex-portal
The Portal is registered here as a confidential OIDC client. Look at:
- Valid redirect URIs —
https://app.iamlab.genbay.co/*. Keycloak will only return a token to an address on this list; that is what stops a look-alike site from harvesting logins. - Web origins — the browser origins allowed to call it.
- Credentials tab — the client secret the Portal uses to prove it is really the Portal.
- Client scopes › dedicated › Mappers — the
groupsmapper that puts entitlements in the token OPA later reads.
Users, Groups and Sessions
Users shows the federated workforce (they appear after a sync, or on first login). Open one and its Groups tab reflects directory membership — read-only, because midPoint owns it.
Sessions lets you see and revoke live logins — the blunt instrument when you need someone out now, before the next governance cycle.
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
- Sign into the Portal as
erin.evans, then find that exact login under Realm settings › Events. - After off-boarding Alice, search for her in Users — she is gone, because of the federation filter. That absence is what the SIEM alert keys on.