DEV Community

Cover image for IAM Development Lab in Keycloak
Shoban Chiddarth
Shoban Chiddarth

Posted on

IAM Development Lab in Keycloak

Introduction

This is the ultimate IAM development and employee lifecycle management lab I am doing where I will be making use of the skills I learnt from the TATA virtual internship via Forage: Cybersecurity Analyst - IAM Developer to develop and implement an IAM solution for a fictional organization.

I chose Keycloak for this lab instead of SailPoint with Oracle Identity Manager because Keycloak is fully open source. This is a standalone lab independent of OpenLDAP.

The internship covered

  • IAM fundamentals
  • Digital identity
  • Authentication
  • Authorization
  • SSO
  • least privilege principle And then walked through designing and planning a full IAM implementation for a fictional enterprise called TechCorp (but the fictional org we will be representing is called Acme). The proposed solution used
  • SailPoint for automated user lifecycle management and
  • Oracle Identity Manager for RBAC with a four-phase implementation plan covering deployment, testing, training, and ongoing monitoring.

This lab takes those same requirements and implements them end-to-end:

  • A structured org with departments and roles
  • RBAC enforced at the role level
  • Full user lifecycle management from provisioning to offboarding
  • MFA
  • SSO via OIDC
  • Email-based verification flows
  • Brute force protection
  • Audit logging All running on-premises in VirtualBox with no external dependencies. ## Pre-Requisites
  1. Superior VM Intercommunication setup
  2. Pi-Hole
  3. Mkcert Local CA TLS (for HTTPS)

Build Log

Keycloak Server Initialization

I cloned a debian server VM, put it in the current VirtualBox host only network I have, assigned a static IP (192.168.57.8), Then I edited its hostname, and mapped DNS record keycloak.acme.internal -> 192.168.57.8 in Pi-Hole.

debian@keycloak:~$ sudo ifconfig enp0s3
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.57.8  netmask 255.255.255.0  broadcast 192.168.57.255
        inet6 fe80::43b5:ca52:a96d:134a  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:8a:4a:29  txqueuelen 1000  (Ethernet)
        RX packets 28569  bytes 3772704 (3.5 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 28550  bytes 2712767 (2.5 MiB)
        TX errors 0  dropped 2 overruns 0  carrier 0  collisions 0

debian@keycloak:~$ nslookup keycloak.acme.internal
Server:     192.168.57.3
Address:    192.168.57.3#53

Name:   keycloak.acme.internal
Address: 192.168.57.8

debian@keycloak:~$ 
Enter fullscreen mode Exit fullscreen mode

Then I installed java on it because Keycloak requires it.

sudo apt install default-jre -y
Enter fullscreen mode Exit fullscreen mode

Java version 21 btw

debian@keycloak:~$ java --version
openjdk 21.0.10 2026-01-20
OpenJDK Runtime Environment (build 21.0.10+7-Debian-1deb13u1)
OpenJDK 64-Bit Server VM (build 21.0.10+7-Debian-1deb13u1, mixed mode, sharing)
Enter fullscreen mode Exit fullscreen mode

Then I downloaded Keycloak from GitHub, extracted it and put it in /opt

debian@keycloak:~/Downloads$ ls
keycloak-26.5.6.tar.gz
debian@keycloak:~/Downloads$ tar -xzf keycloak-26.5.6.tar.gz 
debian@keycloak:~/Downloads$ ls
keycloak-26.5.6  keycloak-26.5.6.tar.gz
debian@keycloak:~/Downloads$ sudo mv keycloak-26.5.6 /opt/keycloak
debian@keycloak:~/Downloads$ cd /opt
debian@keycloak:/opt$ ls
keycloak
debian@keycloak:/opt$ 
Enter fullscreen mode Exit fullscreen mode

And created a new user and set appropriate permissions.

sudo useradd -r -s /sbin/nologin keycloak
sudo chown -R keycloak:keycloak /opt/keycloak
Enter fullscreen mode Exit fullscreen mode

Local CA certificates for Keycloak

I did the usual, created certs for keycloak.acme.internal and moved them to the Keycloak server and pointed Keycloak to it.

debian@keycloak:~/acme-certs$ ls
keycloak.acme.internal-key.pem  keycloak.acme.internal.pem
debian@keycloak:~/acme-certs$ sudo mkdir -p /opt/keycloak/conf/certs
debian@keycloak:~/acme-certs$ sudo cp keycloak.acme.internal.pem /opt/keycloak/conf/certs/keycloak.crt
debian@keycloak:~/acme-certs$ sudo cp keycloak.acme.internal-key.pem /opt/keycloak/conf/certs/keycloak.key
debian@keycloak:~/acme-certs$ sudo chown -R keycloak:keycloak /opt/keycloak/conf/certs
debian@keycloak:~/acme-certs$ sudo chmod 640 /opt/keycloak/conf/certs/keycloak.key
debian@keycloak:~/acme-certs$ sudo nano /opt/keycloak/conf/keycloak.conf 
debian@keycloak:~/acme-certs$ cat /opt/keycloak/conf/
cache-ispn.xml  certs/          keycloak.conf   README.md       truststores/    
debian@keycloak:~/acme-certs$ cat /opt/keycloak/conf/keycloak.conf | head -5
hostname=keycloak.acme.internal
https-certificate-file=/opt/keycloak/conf/certs/keycloak.crt
https-certificate-key-file=/opt/keycloak/conf/certs/keycloak.key
http-enabled=false

debian@keycloak:~/acme-certs$ 
Enter fullscreen mode Exit fullscreen mode

Keycloak initial build

sudo -u keycloak KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin123 /opt/keycloak/bin/kc.sh build
Enter fullscreen mode Exit fullscreen mode

Output:

debian@keycloak:~/acme-certs$ sudo -u keycloak KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=password /opt/keycloak/bin/kc.sh build
WARNING: Usage of the default value for the db option in the production profile is deprecated. Please explicitly set the db instead.
INFO: The following run time options were found, but will be ignored during build time: kc.https-certificate-key-file, kc.http-enabled, kc.hostname, kc.https-certificate-file

Updating the configuration and installing your custom providers, if any. Please wait.
2026-03-19 20:00:27,827 INFO  [io.quarkus.deployment.QuarkusAugmentor] (main) Quarkus augmentation completed in 10529ms
Server configuration updated and persisted. Run the following command to review the configuration:

    kc.sh show-config

debian@keycloak:~/acme-certs$ /opt/keycloak/bin/kc.sh show-config
Current Mode: production
Current Configuration:
    kc.log-level-org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup =  null (Derived)
    kc.log-level-io.quarkus.config =  null (Derived)
    kc.hostname =  keycloak.acme.internal (keycloak.conf)
    kc.log-console-output =  default (classpath application.properties)
    kc.log-level-io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor =  null (Derived)
    kc.log-level-liquibase.database.core.PostgresDatabase =  null (Derived)
    kc.optimized =  true (Persisted)
    kc.version =  26.5.6 (SysPropConfigSource)
    kc.log-level-io.smallrye.openapi.runtime.scanner.dataobject =  null (Derived)
    kc.https-certificate-file =  /opt/keycloak/conf/certs/keycloak.crt (keycloak.conf)
    kc.log-level-org.jboss.resteasy.resteasy_jaxrs.i18n =  null (Derived)
    kc.log-level-io.quarkus.arc.processor.BeanArchives =  null (Derived)
    kc.log-level-io.quarkus.deployment.steps.ReflectiveHierarchyStep =  null (Derived)
    kc.http-enabled =  false (keycloak.conf)
    kc.log-level-org.hibernate.SQL_SLOW =  null (Derived)
    kc.https-certificate-key-file =  /opt/keycloak/conf/certs/keycloak.key (keycloak.conf)
    kc.log-level-io.quarkus.arc.processor.IndexClassLookupUtils =  null (Derived)
    kc.log-file =  /opt/keycloak/bin/../data/log/keycloak.log (classpath application.properties)
    kc.log-level-org.hibernate.engine.jdbc.spi.SqlExceptionHelper =  null (Derived)
debian@keycloak:~/acme-certs$ 
Enter fullscreen mode Exit fullscreen mode

Keycloak daemon build (systemd)

I created a file /etc/systemd/system/keycloak.service with contents

[Unit]
Description=Keycloak Identity and Access Management
After=network.target

[Service]
User=keycloak
Group=keycloak
Environment=KEYCLOAK_ADMIN=admin
Environment=KEYCLOAK_ADMIN_PASSWORD=admin123
ExecStart=/opt/keycloak/bin/kc.sh start
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

And enabled the service

debian@keycloak:~/acme-certs$ sudo systemctl daemon-reload
debian@keycloak:~/acme-certs$ sudo systemctl enable --now keycloak
Created symlink '/etc/systemd/system/multi-user.target.wants/keycloak.service' → '/etc/systemd/system/keycloak.service'.
debian@keycloak:~/acme-certs$ sudo systemctl status keycloak
● keycloak.service - Keycloak Identity and Access Management
     Loaded: loaded (/etc/systemd/system/keycloak.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-03-19 20:05:40 IST; 6s ago
 Invocation: cde0a8f017a24bac8dd23934aa7a5bf2
   Main PID: 3250 (kc.sh)
      Tasks: 29 (limit: 2301)
     Memory: 269.9M (peak: 270.2M)
        CPU: 6.151s
     CGroup: /system.slice/keycloak.service
             ├─3250 /bin/sh /opt/keycloak/bin/kc.sh start
             └─3322 java -Djava.util.concurrent.ForkJoinPool.common.threadFactory=io.quarkus.bootstrap.forkjoin.QuarkusForkJoinW>

Mar 19 20:05:40 keycloak.acme.internal systemd[1]: Started keycloak.service - Keycloak Identity and Access Management.
Mar 19 20:05:46 keycloak.acme.internal kc.sh[3322]: 2026-03-19 20:05:46,328 INFO  [org.hibernate.orm.jdbc.batch] (JPA Startup Th>
debian@keycloak:~/acme-certs$ 
Enter fullscreen mode Exit fullscreen mode

Keycloak admin dashboard

I visited https://keycloak.acme.internal:8443 from where the rootCA.pem was trusted in the OS as well as browser certificate store.

001

Then signed in.

002

And then created a permanent admin account as it says. I created a new user with username administrator, set a password, and then assigned admin role to it. Then logged in with that user and deleted the temporary admin user.

003

Then I created a realm for our fictional org - acme - with display name "Acme Corp", user self-registration disabled (admin-provisioned org), and email verification enabled.

Mailpit initial setup

Since this is a local lab with no real mail infrastructure, I used Mailpit - a lightweight fake SMTP server that catches all outbound emails and displays them in a web UI instead of actually delivering them. This lets me test Keycloak's email flows (verification, password reset, OTP) without setting up a real mail server or domain. I ran it on the same VM as Keycloak since it's a single binary with minimal resource usage.

I installed mailpit from GitHub, on the same VM as Keycloak.

wget https://github.com/axllent/mailpit/releases/download/v1.29.3/mailpit-linux-amd64.tar.gz
mkdir mailpit
tar -xzf mailpit-linux-amd64.tar.gz -C mailpit
cd mailpit
sudo mv mailpit /usr/local/bin/
sudo chmod +x /usr/local/bin/mailpit
Enter fullscreen mode Exit fullscreen mode

And then verified it

$ mailpit version
mailpit v1.29.3 compiled with go1.26.1 on linux/amd64
Error checking for latest release: failed to fetch releases: received status code 403
Enter fullscreen mode Exit fullscreen mode

And I created a new user for it

sudo useradd -r -s /sbin/nologin mailpit
Enter fullscreen mode Exit fullscreen mode

Mailpit daemon build (systemd)

I put the following contents on /etc/systemd/system/mailpit.service (copied the previously generated keys to /etc/mailpit/certs and changed ownership of that folder to mailpit)

# /etc/systemd/system/mailpit.service
[Unit]
Description=Mailpit SMTP and Web UI
After=network.target

[Service]
User=mailpit
Group=mailpit
ExecStart=/usr/local/bin/mailpit --smtp 0.0.0.0:1025 --listen 0.0.0.0:8025 --ui-tls-cert /etc/mailpit/certs/keycloak.acme.internal.pem --ui-tls-key /etc/mailpit/certs/keycloak.acme.internal-key.pem
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

And ran

sudo systemctl daemon-reload
sudo systemctl enable --now mailpit
sudo systemctl status mailpit
Enter fullscreen mode Exit fullscreen mode

It works

debian@keycloak:~/Downloads/mailpit$ sudo systemctl daemon-reload
sudo systemctl enable --now mailpit
sudo systemctl status mailpit
Created symlink '/etc/systemd/system/multi-user.target.wants/mailpit.service' → '/etc/systemd/system/mailpit.service'.
● mailpit.service - Mailpit SMTP and Web UI
     Loaded: loaded (/etc/systemd/system/mailpit.service; enabled; preset: enab>
     Active: active (running) since Fri 2026-03-20 15:37:36 IST; 67ms ago
 Invocation: 1046374b8fb24c4f92c8a6fe9683cb5f
   Main PID: 2010 (mailpit)
      Tasks: 3 (limit: 2301)
     Memory: 8.7M (peak: 8.7M)
        CPU: 13ms
     CGroup: /system.slice/mailpit.service
             └─2010 /usr/local/bin/mailpit --smtp 0.0.0.0:1025 --listen 0.0.0.0>

Mar 20 15:37:36 keycloak.acme.internal systemd[1]: Started mailpit.service - Ma>
Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36>
Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36>
Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36>
Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36>
debian@keycloak:~/Downloads/mailpit$ 
Enter fullscreen mode Exit fullscreen mode

Keycloak to use Mailpit for email

After I created the ACME Corp realm on Keycloak, I went to Realm Settings, email, entered these values to make use of Mailpit as email service.

004

005

Then I visited Mailpit dashboard at https://keycloak.acme.internal:8025/

006

007

It works.

Password Policy in Keycloak

I went to Authentication -> Password Policy -> Password Policies and set the password requirements to have

  • Minimum 10 characters
  • Minimum 1 Uppercase
  • Minimum 1 Lowercase
  • Minimum 1 Digit
  • Minimum 1 Special Character
  • Password history: 3 (prevents reuse of last 3 passwords)

008

I also configured brute force detection under Realm Settings -> Security Defenses -> Brute force detection:

  • Brute Force Mode: Lockout permanently after temporary lockout
  • Max login failures: 5
  • Maximum temporary lockouts: 1
  • Strategy to increase wait time: Multiple
  • Wait increment: 1 minute
  • Max wait: 15 minutes
  • Failure reset time: 12 hours

After 5 failed login attempts the account locks out temporarily. If login failures continue through the temporary lockout, the account gets permanently locked and requires an admin to manually re-enable it. This will be demonstrated later in the user lifecycle section using Burp Suite to capture the login request and a Python script to simulate a brute force attack with a wordlist.

009

Organization Structure

org-structure

I created the above organization structure in Keycloak - four groups for departments, three realm roles (employee, manager, it-admin), and seven users provisioned and assigned accordingly.

All users:

010-all-users

Group Engineering:

011-group-engineering

Group Finance:

012-group-finance

Group HR:

013-group-hr.png)

Group IT:

014-group-it

Role employee:

015-role-employee

Role manager:

016-role-manager

Role IT Admin:

017-role-it-admin

Enforcing MFA via TOTP (Google Authenticator)

I set up password based logins for users (Something You Know). Now I will set up Google Authenticator based 2FA (Something You Have). For this lab, I will be using gauth.apps.gbraad.nl, a browser based Google Authenticator app implementation, on the host machine, as my Google Authenticator client.

I duplicated the default browser authentication flow in Authentication -> Flows, named it acme browser, and set the Browser - Conditional OTP step from Conditional to Required. Then under Authentication -> Bindings, I set the Browser flow to acme browser. This forces TOTP on every login for all users in the acme realm.

I also enabled the following required actions under Authentication -> Required actions:

  • Update Password
  • Verify Email
  • Configure OTP

These are applied to all users, so on first login every user is forced to change their temporary password, verify their email via Mailpit, and set up TOTP before they can access anything.

jsmith is forced to setup Google Authenticator login in next successful login:

018-jsmith-2fa

jsmith added key to Google Authenticator

019-jsmith-ga

jsmith enters the OTP value and logs in successfully. Every other account did the same.

020-everyone-2fa

OIDC Client and SSO Demo

To demonstrate SSO, I registered a client in the acme realm pointing to the official Keycloak demo app hosted at https://www.keycloak.org/app/. Since it's a static page that runs entirely in the browser, it talks directly to my local Keycloak instance - no server-side component needed.

Client settings:

  • Client type: OpenID Connect
  • Client authentication: Off (public client)
  • Valid redirect URIs: https://www.keycloak.org/app/*
  • Valid post logout redirect URIs: https://www.keycloak.org/app/*
  • Web origins: https://www.keycloak.org

021-client-setup

On the demo app, I pointed it at my Keycloak instance by entering the server URL, realm, and client ID.

022-client-config

Clicking Sign In redirected to the Keycloak login page, went through the full authentication flow - password then TOTP - and landed back on the demo app showing jsmith's authenticated session with the decoded token.

023-jsmith-sso-success

Employee lifecycle management

To demonstrate the full employee lifecycle, I walked through four real-world IAM events using mclark from the HR department.

1. mclark gets promoted to HR manager

mclark was promoted from HR employee to HR manager. In Keycloak, this meant removing the employee role and assigning manager under the user's Role Mappings tab.

Before:

024-before-mclark-promotion

After:

025-after-mclark-promotion

2. mclark takes a vacation (account disabled)

mclark went on an extended leave of absence. Rather than deleting the account, it was disabled - access is immediately revoked but the account and its data remain intact for when the employee returns.

026-mclark-vacation

3. mclark returns (account enabled)

mclark returned from leave. The account was re-enabled, restoring access with all roles and group memberships unchanged.

027-mclark-returns

4. mclark quits (account deleted)

mclark resigned. The account was permanently deleted from the realm.

Confirmation:

028-delete-mclark-confirmation

mclark is no more:

029-no-mclark

Brute Force Protection

Now I am going to do a brute force attack on jsmith's account. I am going to manually type in some random password values and hit enter multiple times to see what happens.

030-jsmith-temp-locked

As you can see from the admin dashboard, jsmith is now temporarily locked.

Event log

Keycloak captures two categories of events per realm. Login events record every authentication-related action - successful logins, failed attempts, logouts, and token operations. Admin events record every administrative action performed on the realm - user creation, role assignments, deletions, client configuration changes, and so on. Both are visible under Events in the admin console.

Login Events

Here is a snippet of lwilson signin related events

031-lwilson-signin

Admin Events

Here is a snippet of admin events related to jsmith getting promoted

032-jsmith-gets-promoted

Conclusion

The internship asked for a designed IAM solution and an implementation plan. This lab is that plan executed.

Every requirement from the Tata Forage simulation is covered here in a working deployment:

  • Keycloak handles identity and access management for Acme Corp the same way SailPoint and Oracle Identity Manager would handle it for TechCorp.
  • RBAC is enforced through realm roles mapped to job functions.
  • User provisioning and de-provisioning follow the same lifecycle model - onboarding with required actions, role changes on promotion, account suspension on leave, and full deletion on resignation.
  • MFA via TOTP enforces the "something you know + something you have" authentication standard.
  • SSO via OIDC demonstrates seamless access across applications from a single authenticated session.
  • Mailpit handles all email flows locally.
  • Brute force protection is configured and verified.
  • Admin and login events are captured in the audit log.

The entire stack - Keycloak, Mailpit, TLS via a local CA, DNS via Pi-hole, and network routing via pfSense - runs self-contained in VirtualBox, making it fully reproducible as a home lab without any cloud dependency or paid tooling.

Top comments (0)