Tool reference

Keycloak

The identity provider — single sign-on, MFA and directory federation.

Open source Source github.com/keycloak/keycloak Licence Apache-2.0 Running here quay.io/keycloak/keycloak 26.7.1

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:

SettingValue hereWhy
Display nameGolonexwhat users see on the login page
Require SSLexternalHTTPS enforced for outside requests
User registrationoffidentities come from HR, never self-service
Brute force detectiononlocks an account after repeated failures
User + admin eventsonthe 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:

FieldValue hereWhat it means
Connection URLldap://openldap:389the directory, on the internal network
Bind DNcn=readonly,dc=golonex,dc=locala read-only account — Keycloak never writes
Users DNou=people,dc=golonex,dc=localwhere to look for accounts
Username LDAP attributeuidwhat becomes the Keycloak username
Edit modeREAD_ONLYgovernance belongs to midPoint, not here
Custom user LDAP filter(!(employeeType=disabled))disabled accounts disappear — this is what makes off-boarding instant
Periodic full syncevery 300 skeeps 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 groups mapper 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.

RouteWho uses itGood for
PDP console in the Portalinstructor, IT Opsshowing the policy and its decisions to a room
OPA's REST API from a containerengineersasking the engine directly, with no app involved
Files and scripts on the hostwhoever runs the labediting 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:

PathWhat it is
/opt/ciam-lab/opa/policies/portal.regothe live policy
/opt/ciam-lab/opa/policies/portal.rego.origpristine copy — the restore point for the live-edit demo
/opt/ciam-lab/opa/policies/portal_test.regothe policy's own unit tests
/opt/ciam-lab/opa-ask.shask OPA a question from the command line
/opt/ciam-lab/opa-watch.shlive 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 50001

Watch decisions live (Ctrl-C to stop):

/opt/ciam-lab/opa-watch.sh

Run the policy's unit tests:

docker exec opa /opa test /policies -v

Raw decision log — every decision, with full input and result:

docker logs --tail 20 opa
Editing the file does not reload OPA OPA runs without --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:

RuleLevelFires on
1002014any denied action, naming the user and the reason
1002025a payment approved, with the amount
1002034an HR record changed — workforce data feeding IGA
1002044an invoice created
10020510the authorization policy itself was changed
10021010a 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.