Watch the Full Demo Walkthrough on YouTube
Shows the full JML lifecycle, OpenBao secrets injection, and Elastic SIEM event routing in action.
A production-like Zero Trust Identity & Access Management (IAM) platform demonstrating enterprise IAM workflows using Keycloak SSO (OpenID Connect), OpenBao secrets management, Nginx reverse proxy with TLS, and ELK Stack SIEM integration. Features a complete Joiner/Mover/Leaver (JML) lifecycle engine, access review certification workflow, MFA enforcement, SIEM evidence panels, and impact metrics dashboard.
Every design decision follows Zero Trust principles: never trust, always verify.
This project is a fully containerized, single-command deployment that mirrors the identity security stack used in enterprise environments — built as a portfolio piece demonstrating hands-on IAM engineering skills.
📘 See ARCHITECTURE.md for a detailed technical breakdown of each component and data flow.
This lab models how enterprise Identity & Access Management works in practice. Here's the real-world parallel:
| Lab Feature | Enterprise Equivalent | Real-World Service |
|---|---|---|
| Joiner workflow | New employee onboarding | Azure AD / Okta user provisioning |
| Mover workflow | Internal transfer / role recomputation | SailPoint IdentityNow lifecycle events |
| Leaver workflow | Offboarding / access revocation | ServiceNow ITSM + SCIM deprovisioning |
| Access Reviews | Periodic access certification campaigns | SailPoint Access Certifications, Azure AD Access Reviews |
| MFA enforcement | Step-up authentication for privileged actions | Okta Adaptive MFA, Azure Conditional Access |
| SIEM integration | Identity threat detection & audit trail | Splunk, Microsoft Sentinel, Elastic SIEM |
| KPI Metrics | IAM program health & operational metrics | Identity Governance dashboards |
| RBAC with Keycloak | Centralized role management | Azure AD Roles, Okta Groups, CyberArk |
| Secrets from OpenBao | Runtime secrets management | HashiCorp Vault, AWS Secrets Manager |
| Route | Required Role | MFA Required | Fallback |
|---|---|---|---|
/app/ |
None (public) | No | — |
/app/dashboard |
Authenticated | No | Redirect to login |
/app/secrets |
zero-trust-admin OR zero-trust-user |
No | RBAC denied |
/app/admin |
zero-trust-admin |
Yes | MFA required page |
/app/admin/iam/joiner |
zero-trust-admin |
Yes | MFA required page |
/app/admin/iam/mover |
zero-trust-admin |
Yes | MFA required page |
/app/admin/iam/leaver |
zero-trust-admin |
Yes | MFA required page |
/app/admin/reviews |
zero-trust-admin |
Yes | MFA required page |
/app/admin/reviews/decide |
zero-trust-admin |
Yes | MFA required page |
/app/admin/reviews/export |
zero-trust-admin |
Yes | MFA required page |
MFA Enforcement Modes:
| Mode | Env Variable | Behavior |
|---|---|---|
| Demo (default) | FLASK_ADMIN_MFA_STRICT=false |
Admin actions allowed with visible warning banner; bypass logged to SIEM |
| Strict | FLASK_ADMIN_MFA_STRICT=true |
Admin actions blocked without MFA; redirected to MFA setup page |
| Principle | Implementation |
|---|---|
| 🔍 Never Trust, Always Verify | Every request authenticated via Keycloak OIDC tokens |
| 🔒 Least Privilege | RBAC roles with scoped OpenBao policies (flask-app-policy) |
| ⏱️ Short-Lived Tokens | Access tokens: 5 minutes · Sessions: 1 hour max |
| 🔑 Secrets Never Hardcoded | All secrets fetched at runtime from OpenBao KV engine |
| 📊 All Access Logged | Keycloak events + Nginx logs + SIEM events shipped to ELK |
| 🛡️ TLS Everywhere | Nginx enforces HTTPS with HSTS, CSP, and security headers |
| 🔐 MFA Enforcement | TOTP/OTP enforced for admin actions with configurable strictness |
| 🌐 Network Isolation | Docker networks separate identity and monitoring traffic |
| 👤 Identity Lifecycle | JML workflow manages user onboarding, transfers, and offboarding |
| ✅ Access Certification | Periodic access reviews with approve/revoke + audit trail |
| Component | Technology | Version | Purpose |
|---|---|---|---|
| Identity Provider | Keycloak | 24.0.4 | SSO, OIDC, RBAC, MFA |
| Secrets Engine | OpenBao | 2.0.0 | KV secrets management |
| Reverse Proxy | Nginx | 1.25-alpine | TLS termination, security headers |
| Demo Application | Flask (Python) | 3.0.3 | OIDC-protected web app with IAM workflows |
| IAM Data Store | SQLite | 3.x | JML records, access reviews, SIEM events |
| Database | PostgreSQL | 16-alpine | Keycloak persistent store |
| Search Engine | Elasticsearch | 8.13.4 | Log storage & analysis |
| Dashboards | Kibana | 8.13.4 | SIEM visualization |
| Log Shipper | Filebeat | 8.13.4 | Log collection & forwarding |
| Orchestration | Docker Compose | v2 | Container orchestration |
- Docker (20.10+)
- Docker Compose v2
- OpenSSL (for certificate generation)
- 8GB+ RAM recommended (Elasticsearch needs ~512MB)
# Clone the repository
git clone https://github.com/lenoshz/zero-trust-identity-lab.git
cd zero-trust-identity-lab
# Make scripts executable
chmod +x scripts/bootstrap.sh
# Deploy everything with a single command
./scripts/bootstrap.sh
⚠️ Self-signed certificates: Accept the browser security warning when accessinghttps://localhost.
# 1. Generate TLS certificates
chmod +x nginx/ssl/generate-certs.sh
./nginx/ssl/generate-certs.sh
# 2. Start all services
docker compose up -d
# 3. Wait for services to be healthy (~90 seconds)
docker compose ps
# 4. Seed OpenBao secrets
chmod +x openbao/init/setup.sh
./openbao/init/setup.sh
# 5. Check health
chmod +x scripts/healthcheck.sh
./scripts/healthcheck.sh| Service | URL | Credentials |
|---|---|---|
| 🌐 Demo Application | https://localhost/app |
See demo users below |
| 🔐 Keycloak Login | https://localhost/auth |
Demo users below |
| ⚙️ Keycloak Admin Console | https://localhost/auth/admin |
admin / Admin@ZeroTrust1 |
| 📊 Kibana SIEM | https://localhost/kibana |
elastic / Elastic@ZeroTrust1 |
| 🔑 OpenBao UI | http://localhost:8200 |
Token: root-dev-token-zerotrust |
| Username | Password | Role | Access Level |
|---|---|---|---|
ztadmin |
Admin@123 |
zero-trust-admin |
Full administrative access + IAM workflows |
ztuser |
User@123 |
zero-trust-user |
Standard read/write access |
ztviewer |
View@123 |
zero-trust-readonly |
Read-only access |
Navigate to https://localhost/app and click "Authenticate with Keycloak". You'll be redirected to the Keycloak login page.
Sign in as ztadmin with password Admin@123. Keycloak issues a short-lived JWT access token (5-minute TTL) and redirects you back to the Flask app.
The dashboard displays:
- KPI Metrics: Onboarding time, offboarding completion, privileged accounts, MFA coverage, review completion with trend arrows
- Your authenticated username, email, and role assignments
- Token expiry countdown with live status chip
- OpenBao connection status
- Audit Log: Session-scoped events
- Live Health: Per-service response codes and latency
- Security Events (SIEM): Persistent security event timeline with filter chips and Kibana deep-link
- Joiner (
/app/admin/iam/joiner): Create a new user → auto-assigns baseline role by department - Mover (
/app/admin/iam/mover): Transfer user to new department → role automatically recomputed - Leaver (
/app/admin/iam/leaver): Disable user → all roles revoked, sessions invalidated
Navigate to /app/admin/reviews:
- Review each user's role assignment with risk level tagging (Low / Medium / High / Critical)
- Approve role assignments with one click
- Revoke elevated access with mandatory justification comment
- Export all review decisions as CSV for compliance evidence
Navigate to /app/secrets to see application secrets fetched in real-time from OpenBao's KV v2 engine. All values are masked — never exposed in source code.
- Failed login: Try logging in with the wrong password — Keycloak logs
LOGIN_ERROR - Admin action: Access the admin panel — logged as
admin_action - JML event: Create/move/disable a user — logged with full audit detail
- Access review: Approve or revoke — logged as
review_decision - MFA bypass: Access admin in demo mode — logged as
mfa_bypass
All events are visible in the Dashboard's Security Events panel and flow to ELK via Filebeat.
Navigate to https://localhost/kibana and explore the zerotrust-logs-* index pattern to see real-time identity events. Use the "Open in Kibana" button from the dashboard for pre-filtered queries.
1. Log in as ztadmin
2. Navigate to /app/admin/iam/joiner
3. Create user: username=testuser1, email=test@co.com, name=Test User, dept=Engineering
4. Verify: success message shown, role auto-assigned = zero-trust-user
5. Verify: user appears in Recent Joiners table
6. Verify: security event logged in SIEM panel
1. Navigate to /app/admin/iam/mover
2. Select testuser1 → see current: Engineering / zero-trust-user
3. Change to department: Security
4. Verify: transfer preview shows Engineering→Security, role→zero-trust-admin
5. Submit → verify success, role recomputed
6. Verify: movement history updated
1. Navigate to /app/admin/iam/leaver
2. Select testuser1
3. Review revocation checklist → confirm disable
4. Verify: user disabled, roles revoked
5. Verify: user appears in Disabled Users table
6. Verify: user no longer in Mover/Joiner active user lists
1. Navigate to /app/admin/reviews
2. Review ztadmin → Risk Level = CRITICAL → Approve
3. Review ztuser → Risk Level = MEDIUM → Revoke with reason: "Access no longer needed"
4. Verify: review history updated with decision + reviewer + timestamp
5. Click Export CSV → verify downloaded file contains decisions
6. Verify: review completion percentage updates
# Demo mode (default):
1. Log in as ztadmin → access /app/admin
2. Verify: yellow "MFA Not Verified" warning banner shown
3. Verify: admin actions still functional
4. Verify: mfa_bypass event logged in security events
# Strict mode:
1. Set FLASK_ADMIN_MFA_STRICT=true in .env
2. docker compose up -d flask-app
3. Access /app/admin → verify: redirected to MFA Required page
4. Verify: mfa_denied event in security events
| Metric | What It Measures | Why It Matters |
|---|---|---|
| Avg. Onboarding Time | Simulated time from joiner creation to role assignment | Slow onboarding = security gap (no access) or risk (rushed permissions) |
| Offboarding Completion % | % of disabled users with all roles fully revoked | Incomplete offboarding = orphaned accounts = security risk |
| Privileged Accounts | Count of users with zero-trust-admin role |
Excessive privilege = expanded attack surface |
| MFA Coverage % | % of active users with MFA configured (simulated) | Low MFA = credential theft risk |
| Review Completion % | % of users with a completed access review | Low completion = stale entitlements = compliance gap |
Each KPI shows a trend arrow (↑/↓/→) comparing the current value to the prior snapshot:
- Green ↑/↓: Improvement (higher completion %, lower onboarding time, fewer privileged accounts)
- Red ↑/↓: Degradation
- Gray →: No significant change
| This Lab | Azure AD / Entra ID | Okta | SailPoint |
|---|---|---|---|
| Keycloak OIDC SSO | Azure AD App Registration | Okta OIDC Integration | — |
| JML Joiner | Azure AD user provisioning via SCIM | Okta lifecycle hooks | IdentityNow Joiner Rule |
| JML Mover | Azure AD dynamic groups | Okta profile attribute mapping | IdentityNow Mover Rule |
| JML Leaver | Azure AD user disable + licence revoke | Okta deactivate user | IdentityNow Leaver Rule |
| Access Reviews | Azure AD Access Reviews | Okta Access Certifications | Access Campaigns |
| MFA Enforcement | Conditional Access + MFA challenge | Adaptive MFA policies | — |
| Security Events | Azure AD Sign-in Logs + Sentinel | Okta System Log | Activity Monitoring |
| KPI Dashboard | Identity Governance Analytics | Okta Reports | IdentityNow Dashboards |
| OpenBao Secrets | Azure Key Vault | Okta Credential Management | — |
| RBAC | Azure AD Roles + Privileged Identity Mgmt | Okta Groups + App Assignments | Entitlements |
zero-trust-identity-lab/
├── docker-compose.yml # Full stack orchestration
├── .env # Environment secrets (gitignored)
├── .env.example # Template with placeholder values
├── README.md # This file
├── ARCHITECTURE.md # Detailed architecture documentation
├── nginx/
│ ├── nginx.conf # TLS reverse proxy configuration
│ └── ssl/
│ └── generate-certs.sh # Self-signed certificate generator
├── keycloak/
│ ├── realm-export.json # Pre-configured realm with users/roles
│ └── themes/ # Custom theme placeholder
├── openbao/
│ ├── config/openbao.hcl # Server configuration
│ └── init/setup.sh # Secret seeding script
├── flask-app/
│ ├── Dockerfile # Python 3.12 container image
│ ├── requirements.txt # Pinned Python dependencies
│ ├── app.py # OIDC + OpenBao + IAM workflows
│ ├── iam_store.py # SQLite IAM data layer
│ └── templates/
│ ├── index.html # Public landing page
│ ├── dashboard.html # Dashboard + SIEM + KPIs
│ ├── login_required.html # Auth/session failure page
│ ├── mfa_required.html # MFA enforcement page
│ ├── iam_joiner.html # JML Joiner workflow
│ ├── iam_mover.html # JML Mover workflow
│ ├── iam_leaver.html # JML Leaver workflow
│ └── access_reviews.html # Access review certifications
├── elk/
│ ├── elasticsearch/ # ES single-node config
│ ├── kibana/ # Kibana config with base path
│ └── filebeat/ # Log shipping configuration
├── scripts/
│ ├── bootstrap.sh # One-command deployment
│ ├── keycloak-configure.sh # Realm verification
│ └── healthcheck.sh # Service status checker
└── docs/
├── screenshots/ # UI screenshots (post-deployment)
├── zero-trust-flow.md # Authentication flow diagram
└── setup-guide.md # Detailed setup & Kibana guide
See docs/setup-guide.md for detailed troubleshooting, including:
- Docker networking issues
- Keycloak startup failures
- Certificate problems
- Elasticsearch heap issues
- OpenBao connection errors
If OIDC login loops, callback fails, or shows invalid redirect errors, verify the flask-demo-app client in Keycloak:
- Client type / access type: Confidential
- Client authentication: Enabled
- Valid redirect URIs:
https://localhost/app/callbackhttps://localhost/app/*http://localhost:5000/callback(optional direct fallback)
- Web origins:
https://localhosthttp://localhost:5000(optional)
These values must match the Flask app OIDC environment values from docker-compose.yml.
docker compose logs --tail=200 flask-app
docker compose logs --tail=200 nginx
curl -kI https://localhost/auth/realms/zero-trust-realm/.well-known/openid-configuration
curl -kI https://localhost/app/Expected quick checks:
- Discovery endpoint returns
HTTP/1.1 200 OK - App endpoint returns
HTTP/1.1 200 OK - Flask logs show resolved OIDC settings and login redirect URI
- Nginx logs show
/auth/and/app/requests without upstream resolution crashes
# 1) Verify session token timing fields are populated after login callback
docker compose logs --tail=200 flask-app
# 2) Verify app health endpoint includes live check metadata
curl -k https://localhost/app/health
# 3) Verify RBAC guards
# - /app/secrets allows zero-trust-admin and zero-trust-user
# - /app/admin allows zero-trust-admin only
curl -kI https://localhost/app/secrets
curl -kI https://localhost/app/admin
# 4) Verify IAM routes exist (302 redirects to login when unauthenticated)
curl -kI https://localhost/app/admin/iam/joiner
curl -kI https://localhost/app/admin/iam/mover
curl -kI https://localhost/app/admin/iam/leaver
curl -kI https://localhost/app/admin/reviews
# 5) Verify OpenBao access with app-scoped token from Flask container
docker compose exec -T flask-app python -c "import requests; tok=open('/run/secrets/openbao/flask-app-token','r',encoding='utf-8').read().strip(); base='http://openbao:8200/v1'; hdr={'X-Vault-Token':tok}; print('db',requests.get(f'{base}/secret/data/flask-app/db',headers=hdr,timeout=5).status_code); print('api',requests.get(f'{base}/secret/data/flask-app/api',headers=hdr,timeout=5).status_code); print('config',requests.get(f'{base}/secret/data/flask-app/config',headers=hdr,timeout=5).status_code)"
# 6) Verify SQLite database exists
docker compose exec flask-app ls -la /app/data/iam.db# View all container logs
docker compose logs -f
# Restart a specific service
docker compose restart keycloak
# Full reset (WARNING: destroys all data)
docker compose down -v && docker compose up -d
# Check container health
docker compose psMIT License — see LICENSE for details.










