A Docker lab that reproduces real single sign-on and network login failures on demand: SAML, OIDC, LDAP, RADIUS, proxies and APIs. Each failure has a support runbook: the symptom, where to look, the fix and what to ask the customer. An automated test breaks every scenario, checks the user sees the right error, fixes it and proves the login works again.
When enterprise SSO breaks, support usually gets only "SSO stopped working for everyone". The cause is buried in signed XML, tokens, certificates, clocks and two teams' logs, and this lab makes it safe to practise finding it.
![]() |
Runbook 02: clock skew Symptom: the user signs in at the IdP, then the app refuses the assertion as expired. Where to look: the app's clock against real UTC. date -u; docker compose exec saml-sp date -uFix: correct the clock on the app's host. Don't widen the tolerance to hide it. |
A broken login from a real scripts/test.sh run, beside the start of its runbook. | |
New here? Read the walkthrough's chapters 1 and 2: a real support ticket followed from the first message to the fix, in about 10 minutes.
flowchart LR
browser(["Browser"])
proxies["Reverse and egress proxies<br/>nginx, Squid"]
apps["Apps and API<br/>SAML, OIDC, resource server"]
kc["Keycloak<br/>identity provider"]
ldap[("OpenLDAP<br/>directory")]
radius["NAS and FreeRADIUS<br/>network login"]
browser --> apps
browser --> proxies
browser -- "login form" --> kc
proxies --> apps
proxies --> kc
apps -- "tokens, keys" --> kc
kc -- "users, groups" --> ldap
radius -- "users" --> ldap
Keycloak is the identity provider. It federates users from an OpenLDAP directory. SAML and OIDC apps and an API sit in front of it, some behind an nginx reverse proxy and one behind a Squid egress proxy. A NAS logs the same directory users in over RADIUS. The full architecture shows every container, network and channel.
One scenario from start to finish: the SAML app's clock runs 10 minutes fast.
- Break it:
scripts/break.sh clock-skew. Only the app's container clock moves. - See it: in a private window, open
https://idlab.home:8181, click Log in with SAML and sign in as alice. The app refuses:Could not validate timestamp: expired. Check system clock. - Diagnose it: runbook 02 says where to look.
date -u; docker compose exec saml-sp date -ushows the two clocks 10 minutes apart. - Fix it:
scripts/fix.sh clock-skew, then log in again. It works. - Prove it:
scripts/test.shdoes all of this for every scenario, with a real browser.
Prerequisites: Linux with Docker Engine and Compose v2, plus bash, openssl, curl and jq. It uses host ports 8180 to 8189.
git clone https://github.com/MarkHawk/identity-lab && cd identity-lab
scripts/setup.sh # CA, certificates and random secrets -> .env and certs/ (prints no secrets)
# set LAB_HOST=localhost first if you have no DNS name for this machine
docker compose up -d --wait # the whole lab; the first build takes 10 minutes or more (Squid is compiled from source)
scripts/pin-idp-cert.sh # "onboard" the SAML app: trust the IdP's current signing certificate
scripts/status.sh # service health and the live state of every scenario
docker compose exec nas nas-auth dave # a RADIUS login from the NAS; enter the demo password -> "nas: Access-Accept"Then open the apps (HTTPS only, with a certificate warning to accept) and sign in as a demo user.
No Linux machine? Run it in GitHub Codespaces, step by step.
scripts/break.sh <scenario> breaks it, scripts/fix.sh <scenario> fixes it. The scenario reference has what each one breaks and its exact log lines.
| # | Scenario | What the user sees | Runbook |
|---|---|---|---|
| 1 | expired-signing-cert |
SAML app: IdP signing certificate expired on <date> |
01-expired-signing-cert |
| 2 | clock-skew |
SAML app: Could not validate timestamp: expired. Check system clock. |
02-clock-skew |
| 3 | redirect-uri-mismatch |
Keycloak: Invalid parameter: redirect_uri, before the login form |
03-redirect-uri-mismatch |
| 4 | ldap-bind-failure |
Keycloak, directory users only: Unexpected error when handling authentication request to identity provider. |
04-ldap-bind-failure |
| 5 | radius-shared-secret-mismatch |
NAS: RADIUS server not responding. Web sign-in still works. |
05-radius-shared-secret-mismatch |
| 6 | proxy-bad-gateway |
nginx: 502 Bad Gateway. The app behind it is healthy. |
06-proxy-bad-gateway |
| 7 | proxy-gateway-timeout |
nginx: 504 Gateway Time-out, 5 s after the password |
07-proxy-gateway-timeout |
| 8 | proxy-rate-limit |
nginx: 429 Too Many Requests when the password is submitted |
08-proxy-rate-limit |
| 9 | proxy-forwarded-headers |
Keycloak: Invalid Request. OIDC logins post to port 443. |
09-proxy-forwarded-headers |
| 10 | proxy-header-too-large |
nginx: 502 Bad Gateway where the login page should be |
10-proxy-header-too-large |
| 11 | proxy-auth-required |
App: Tunnel connection failed: 407 Proxy Authentication Required |
11-proxy-auth-required |
| 12 | proxy-policy-block |
App: Tunnel connection failed: 403 Forbidden |
12-proxy-policy-block |
| 13 | proxy-tls-interception |
App: certificate verify failed: self-signed certificate in certificate chain |
13-proxy-tls-interception |
| 14 | proxy-bypass |
App: Failed to resolve 'keycloak'. The proxy logs nothing. |
14-proxy-bypass |
| 15 | redirect-loop |
Browser: ERR_TOO_MANY_REDIRECTS, before any login |
15-redirect-loop |
| 16a | api-bearer-audience |
API: 401, invalid_token: the audience doesn't include idlab-api. Login works. |
16-api-bearer-401 |
| 16b | api-bearer-expired |
API: 401, invalid_token: the token expired, 10 s after login |
16-api-bearer-401 |
| 17 | proxy-maintenance |
Proxy: Sign-in is down for planned maintenance (503). Health checks stay green. |
17-service-unavailable |
| 18a | client-cert-missing |
Partner system: 400 Bad Request, No required SSL certificate was sent |
18-client-cert |
| 18b | client-cert-expired |
Partner system: 400 Bad Request, The SSL certificate error |
18-client-cert |
| 19 | cors-blocked |
Page: Failed to fetch; console: blocked by CORS policy. The API logs a 200. |
19-cors-blocked |
- Walkthrough: support tickets from first message to fix, with every tool. Start with chapters 1 and 2.
- Operating the lab: URLs, demo users, first logins, tests, rotating secrets, upgrading, troubleshooting.
- Architecture: the full diagram, each component and channel, and the repository layout.
- Design decisions: why it's built this way, and what testing against real software changed.
- Known limitations: what it doesn't do, or does differently from production.
- Scenarios and the tools that help: which related tool helps with each scenario.
- Codespaces guide and CHANGELOG.
- token-inspector: decodes a SAML response or JWT offline and explains what's wrong with it.
- identity-support-splunk: a Splunk app that shows when a failure started, who is affected and the likely cause, with the runbook.
- rate-limit-probe: tells whether a 429 is the client's fault or the API's.
- auth-capture-explainer: explains a RADIUS or LDAP packet capture offline.
- triage-assistant: reads a ticket, finds the matching runbook and drafts the reply.
This project was built with AI-assisted tooling (Claude Code). I directed the scope, design and review. The assistant wrote much of the code and documentation, and every behaviour claimed here is checked by scripts/test.sh. Several design changes, such as the certificate pinning and the libfaketime monotonic-clock workaround, came from tests failing against the real software rather than from the first plan.
