5.1 Decide before you click
The most common mistake in an IAM project is opening a console first. Every decision below has to be made once, on paper, before any tool is touched — because each one gets baked into a dozen places and is painful to change later.
| Decision | What Golonex chose | Why it matters later |
|---|---|---|
| Authoritative source | The HR feed — one CSV | Everything downstream is a consequence of it. If two systems can both create people, you have no source of truth. |
| Directory layout | ou=people, ou=groups, ou=service | Humans, entitlements and non-human accounts separated from day one — you cannot retrofit this cleanly. |
| Naming convention | Login first.last; groups area-function (finance-ap-clerk) | Predictable names make rules, reports and reviews readable. Inconsistent naming is the reason access reviews fail. |
| Identifier | Employee number (E1004) | Names change; the identifier must not. Correlation depends on it. |
| Birthright vs requested | Everyone gets an account + app-users; everything else is requested | Defines the floor of least privilege. Too generous a birthright is the most common audit finding. |
| Scoping | Entitlements bound to a department | This is what makes the mover case work automatically instead of relying on someone remembering. |
| Segregation of duties | AP Clerk ⟂ AP Approver | Write the toxic pairs down before building roles; retrofitting SoD across live entitlements is a project in itself. |
| Who approves | Members of access-approvers | An approval workflow with no defined approver silently grants or silently blocks. |
| Account status | One attribute — employeeType = active / disabled | Off-boarding needs a single switch every system agrees on. |
5.2 Build the directory skeleton
Start with structure, not people. Three organisational units, and nothing else:
dn: ou=people,dc=golonex,dc=local
objectClass: organizationalUnit
ou: people
description: All human identities (workforce)
dn: ou=groups,dc=golonex,dc=local
objectClass: organizationalUnit
ou: groups
description: Access groups (entitlements) - managed by midPoint
dn: ou=service,dc=golonex,dc=local
objectClass: organizationalUnit
ou: service
description: Non-human / service accountsThen create the entitlement groups — the access model, before anyone holds it. Each is
a groupOfNames, and each description records its purpose and any SoD relationship, so
the rule survives staff turnover:
dn: cn=finance-ap-clerk,ou=groups,dc=golonex,dc=local
objectClass: groupOfNames
objectClass: top
cn: finance-ap-clerk
description: Finance - create and submit supplier invoices (SoD: exclusive with finance-ap-approver)
member: uid=erin.evans,ou=people,dc=golonex,dc=localgroupOfNames must have at least one member — you cannot create an empty
one, which is why a seed member appears above. And by default this directory denies anonymous
read: every other tool binds with a named account, never anonymously.In a real deployment you would stop here — people arrive from HR through provisioning, not from an LDIF. The lab seeds a handful of employees so there is something to look at on day one.
5.3 Configure midPoint — the governance layer
This is the largest piece of configuration in the lab, and the right place to spend your time. Build it in four moves.
Step 1 — connect the authoritative source
Define the HR feed as a resource and map each column onto an identity attribute. The critical parts are not the attribute mappings but these three:
| Setting | Golonex value | What it decides |
|---|---|---|
| Unique attribute | empno | how a row is matched to an existing identity |
| Correlation | employee number, then login | whether an import updates someone or creates a duplicate |
| Activation mapping | status → enabled / disabled | the leaver switch |
Then define the synchronisation reactions — what midPoint does with each kind of row:
| Situation | Reaction | Meaning |
|---|---|---|
unmatched | add focus | a new HR row creates a new identity — the joiner |
linked | synchronise | an existing person changed — the mover |
unlinked | link | an identity already exists; connect them |
deleted | inactivate focus | the row vanished — disable, never delete |
Step 2 — connect the provisioning target
Define the directory as a second resource, this time with a write account. Two design points are worth copying into any real build:
- Entitlements are an association, not an attribute. Group membership is modelled as a relationship between the account and a group object, so midPoint can add and remove membership precisely instead of overwriting a whole attribute.
- Account status is simulated. This directory has no native "disabled" flag, so one is
declared:
employeeType=active/disabled. Every downstream tool then agrees on one switch.
Step 3 — automate the birthright
Nobody should hand-assign baseline access. An object template does it: every identity that comes from HR is tagged, and the tag grants the Employee role automatically.
<mapping>
<name>birthright-employee-role</name>
<strength>strong</strength>
<authoritative>true</authoritative>
<source><path>subtype</path></source>
<expression>
<assignmentTargetSearch>
<targetType>RoleType</targetType>
<filter><q:equal>
<q:path>name</q:path><q:value>Employee</q:value>
</q:equal></filter>
</assignmentTargetSearch>
</expression>
<target><path>assignment</path></target>
<condition><script><code>
subtype != null && subtype.contains('employee')
</code></script></condition>
</mapping>authoritative is the important word: because the template owns this assignment, if the
condition stops being true the role is removed again. Automation that only ever grants is how
access creep starts.
Step 4 — build the role model
Roles are where policy lives. Golonex uses three kinds:
| Kind | Example | Configuration |
|---|---|---|
| Birthright | Employee | not requestable; grants the account and app-users |
| Requestable, approved | HR Staff, IT Admin | requestable + an approval policy rule |
| Requestable, SoD-paired | AP Clerk, AP Approver | requestable + a mutual exclusion rule |
A role grants access through an inducement — "whoever holds me gets this group". Then two policy rules do the governance work. Segregation of duties is remarkably small:
<policyRule>
<name>SoD: Finance AP Clerk excludes Finance AP Approver</name>
<policyConstraints>
<exclusion>
<targetRef oid="...203" type="RoleType"/> <!-- the AP Approver role -->
</exclusion>
</policyConstraints>
<policyActions>
<enforcement/> <!-- block it outright; the alternative is to warn and record -->
</policyActions>
</policyRule><enforcement/> refuses the request. Swapping it for an approval or a soft violation lets
the grant proceed but records it for review. Which you choose is a risk decision, not a technical one —
and the exam expects you to know both exist.Department scoping is what makes the mover case automatic. The inducement carries a condition:
<condition>
<source><path>$focus/organizationalUnit</path></source>
<expression><script><code>
basic.stringify(organizationalUnit) == 'Finance'
</code></script></expression>
</condition>The person keeps the role, but it stops being effective the moment their department changes — so the directory group is removed without anyone raising a ticket.
Finally, two tasks: an import task that reads the HR feed, and a reconciliation task that compares the directory against governed identities. In production the import runs on a schedule; in the lab both are run by hand so you can watch cause and effect.
app-users group. Then try to give one person both AP
roles — it must be refused.5.4 Configure Keycloak — authentication
Keycloak is configured after the directory has content, because it federates rather than owns. Four things, in order:
- Create a realm (
golonex) — a tenant boundary. Turn off self-registration (identities come from HR), turn on brute-force protection, and turn on event logging: without events there is nothing for the SIEM to detect. - Add LDAP user federation pointing at the directory, binding with the
read-only account and set to
READ_ONLYedit mode. This is the single most important choice on the page — it makes governance one-directional. - Add mappers so directory attributes become user attributes, and a group mapper so directory groups become Keycloak groups.
- Register the application as an OIDC client with strict redirect URIs, and add a
groupsmapper so entitlements travel in the token.
Then one line that carries the whole leaver story:
Custom user LDAP filter: (!(employeeType=disabled))user_not_found event rather than a bad-password event, which is precisely the
signal the SIEM rule keys on. Detection was designed at the same time as the control.groups claim must list their directory groups.5.5 Configure authorization — OPA and the application
Authentication tells you who; authorization decides what. Keep them separate: the application asks, the policy engine answers.
Write the policy as code, in three layers:
- Default deny. Start from
default allow := false. Everything permitted must be stated explicitly. - RBAC rules — does the user hold the required group?
- ABAC rules — conditions on the request itself, such as an amount threshold.
Then add the cross-cutting rule that overrides everything else — segregation of duties, enforced a second time at the application:
default sod_violation := false
sod_violation if {
"finance-ap-clerk" in input.groups
"finance-ap-approver" in input.groups
}
# an approver may approve; above 50,000 a manager is also required
allow if {
input.action == "payment.approve"
"finance-ap-approver" in input.groups
not sod_violation
to_number(object.get(input, "amount", 0)) <= 50000
}The application's own configuration is deliberately thin: the OIDC client credentials, the address of the policy engine, and nothing else. It stores no passwords and makes no access decisions — it asks before every action and enforces the answer.
5.6 Configure privileged access
Privileged access is configured separately from ordinary access, because the controls are different: short-lived, MFA-protected and recorded.
In the PAM tool, a role is a small, readable grant:
kind: role
version: v7
metadata:
name: golonex-pam-ops
spec:
allow:
logins: ["golonex-ops"] # the only OS account you may become
node_labels:
env: ["production"] # only servers carrying this label
options:
max_session_ttl: 8h # access expires
record_session:
default: best_effort # recording is not optionalFour lines of policy express what most organisations struggle to enforce: no shared root, no permanent access, no unlabelled scope, no unrecorded session. Add users through an invite so each sets their own password and enrols a second factor — no shared credentials.
For the browser gateway, the equivalent decision is credential injection: the target's password is stored in the connection, not given to the user. Turn on recording (including keystrokes) when you create the connection, not afterwards.
5.7 Configure monitoring
Monitoring is configured last, but it is designed first — you decide what you want to detect while you are building the controls, not after an incident.
- Get the events in. Authentication events from the IdP and authorization decisions from the application are written as JSON to a file the SIEM ingests. Structured logs beat free text: rules can match fields instead of guessing at strings.
- Write rules that express an expectation. A rule is not magic — it is a sentence about what should not happen.
| Expectation | Signal | Level |
|---|---|---|
| Nobody should log in as a terminated employee | login error user_not_found | 12 — high |
| Nobody should hold conflicting finance roles | portal denial with sod_violation | 10 |
5.8 Adapting this to a real organisation
The Golonex model is deliberately small. Scaling it up changes size, not shape:
| In this lab | In production |
|---|---|
| A CSV as the HR feed | A real HR system over an API or database view — the same authoritative-source role |
| Six entitlement groups | Hundreds. Group them into business roles so people request a job function, not a list of permissions. |
| One SoD pair | A maintained matrix of toxic combinations, reviewed by the business, not by IT |
| Approval by one group | Multi-stage approval — line manager, then system owner, then risk for sensitive access |
| Manual task runs | Scheduled import; reconciliation on a regular cycle with findings assigned to owners |
| Seeded users in the directory | Nothing seeded — every account arrives through provisioning, and anything that did not is an orphan |