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
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:~$
Then I installed java on it because Keycloak requires it.
sudo apt install default-jre -y
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)
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$
And created a new user and set appropriate permissions.
sudo useradd -r -s /sbin/nologin keycloak
sudo chown -R keycloak:keycloak /opt/keycloak
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$
Keycloak initial build
sudo -u keycloak KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin123 /opt/keycloak/bin/kc.sh build
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$
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
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$
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.
Then signed in.
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.
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
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
And I created a new user for it
sudo useradd -r -s /sbin/nologin mailpit
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
And ran
sudo systemctl daemon-reload
sudo systemctl enable --now mailpit
sudo systemctl status mailpit
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$
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.
Then I visited Mailpit dashboard at https://keycloak.acme.internal:8025/
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)
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.
Organization 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:
Group Engineering:
Group Finance:
Group HR:
Group IT:
Role employee:
Role manager:
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:
jsmith added key to Google Authenticator
jsmith enters the OTP value and logs in successfully. Every other account did the same.
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
On the demo app, I pointed it at my Keycloak instance by entering the server URL, realm, and client ID.
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.
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:
After:
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.
3. mclark returns (account enabled)
mclark returned from leave. The account was re-enabled, restoring access with all roles and group memberships unchanged.
4. mclark quits (account deleted)
mclark resigned. The account was permanently deleted from the realm.
Confirmation:
mclark is no more:
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.
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
Admin Events
Here is a snippet of admin events related to jsmith getting 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)