필사 모드: From SiteMinder to Keycloak — A Legacy SSO Migration Strategy and Practical Roadmap
EnglishIntroduction — Time to Tackle the Homework You Kept Postponing
In the previous article we dissected the architecture of SiteMinder. This article covers the next question: **"So how do we get out?"**
As of 2026, the reasons you can no longer postpone this homework are clear. [Keycloak 26.6](https://www.keycloak.org/docs/latest/release_notes/index.html) ships FAPI 2.0 Final, passkeys integrated into the login flow, zero-downtime rolling updates, and Workflows-based realm management automation — a level that comfortably absorbs enterprise requirements. On the standards side, the [OAuth 2.1 draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) and [RFC 9700](https://datatracker.ietf.org/doc/html/rfc9700) have consolidated best practices. Meanwhile, on the SiteMinder side, the 12.8.04 EOS (December 2023) shows that version lifecycle management is itself a standing risk, and post-Broadcom licensing changes have made cost forecasting difficult.
This article aims to be a practitioner's roadmap, not a consulting deck. Inventory methodology, mapping SiteMinder objects to Keycloak, coexistence architecture, password migration, and rollback planning — organized in the order you will actually hit them in a real project.
Migration Drivers — Why Leave
Every organization differs, but the drivers converge on three things.
1. EOS and Support Risk
- 12.8.04 reached EOS in December 2023. Environments stuck on old versions are exposed to security patch gaps.
- Upgrading to 12.9 is itself no small project, so the conclusion "if we are doing major surgery anyway, let us move to a standards-based stack" follows naturally.
2. License Cost
- Per-user licensing plus maintenance commonly runs into hundreds of thousands of dollars annually.
- Keycloak is open source (Apache 2.0), and if commercial support is needed there are options such as the Red Hat build of Keycloak. The cost structure shifts from "licenses" to "operational capability."
3. Lack of Standards and Ecosystem Isolation
- SMSESSION is proprietary, making integration with modern workloads — mobile apps, SPAs, APIs, AI agents (non-human identities) — difficult.
- Keycloak, centered on OIDC/SAML/SCIM standards, has structurally lower integration costs with new services.
- Hiring SiteMinder operators gets harder every year, while Keycloak experience keeps growing in the market.
Phase 0: Inventory — Everything Starts With Classification
The most common cause of migration failure is not technology but a **poor inventory**. Dump the policy store with XPSExport and classify applications into the following categories as your starting point.
Full policy store export
XPSExport sm-policy-export.xml -xb -vT
First-pass statistics, e.g. realm/response counts
grep -c "<Realm" sm-policy-export.xml
grep -o 'SM_[A-Z_]*' sm-policy-export.xml | sort | uniq -c | sort -rn
Application Classification Scheme
| Category | Characteristics | Migration difficulty | Approach |
| --- | --- | --- | --- |
| A. Standard-protocol apps | Already integrated via SAML/OIDC (federation partners) | Low | Just repoint the IdP endpoint to Keycloak |
| B. Header-based apps (modifiable) | Depend on SM_USER headers, source can be changed | Medium | Rewrite as OIDC clients, or proxy |
| C. Header-based apps (unmodifiable) | Vendor gone, no source, change-frozen | High | Reproduce headers via proxy pattern (no code change) |
| D. Agent-API-dependent apps | Call the SiteMinder SDK/Agent API directly | Highest | Case-by-case analysis and redesign |
Alongside the classification, collect the following metadata for each app:
- The list of headers it depends on (only SM_USER? also SM_USERGROUPS and custom response headers?)
- Session timeout requirements (the realm's idle/max values)
- Authentication scheme (forms? certificates? Kerberos? any step-up?)
- User store (which LDAP/AD it reads, user attribute dependencies)
- Traffic volume and business criticality (used to decide migration order)
Keep this inventory not in a spreadsheet but as **version-controlled structured data** (YAML/JSON). It becomes the data source for your migration progress dashboard.
app-inventory.yaml (example)
- appId: hr-payroll
category: C # header-based, unmodifiable
headers: [SM_USER, SM_USERGROUPS, X-Custom-Empno]
authScheme: forms
sessionIdle: 30m
owner: hr-it-team
criticality: high
wave: 3 # assigned migration wave
Big Bang vs Phased Migration
| Aspect | Big bang | Phased (recommended) |
| --- | --- | --- |
| Duration | Short (in theory) | Long (1-3 years) |
| Risk | Possible enterprise-wide simultaneous outage | Localized per wave |
| Rollback | Practically impossible | Possible per wave |
| Coexistence infrastructure | Not needed | SAML bridges/proxies required |
| Suitable for | Small estates under ~30 apps | Banks/large enterprises with hundreds of apps |
With more than a few dozen apps, a big bang is a gamble. Everything below assumes a phased migration. Its central challenge is: **during the transition, two IdPs coexist, yet users must log in only once.**
Coexistence Architecture — A Bridge Between Two Worlds
SAML Brokering: Mutual Integration of Keycloak and SiteMinder
The fact that both systems speak SAML 2.0 is the bridge. There are two directions.
**Direction 1: Keycloak as IdP, SiteMinder as SP (the recommended direction toward the target state)**
New and migrated apps log in directly against Keycloak; when the user accesses a legacy app still under SiteMinder, SiteMinder acts as a SAML SP and delegates authentication to Keycloak. The source of truth for authentication moves to Keycloak first, which means modern factors like passkeys can be rolled out enterprise-wide early.
User
|
| 1. Login (passkey/OTP/password)
v
+------------------+ SAML assertion +--------------------+
| Keycloak | -------------------> | SiteMinder |
| (new IdP) | (SM as SAML SP) | (legacy WebSSO) |
+--------+---------+ +---------+----------+
| |
| OIDC/SAML | SMSESSION + SM_USER
v v
+------------------+ +--------------------+
| Migrated apps | | Unmigrated legacy |
| (OIDC clients) | | apps (header-based)|
+------------------+ +--------------------+
**Direction 2: SiteMinder as IdP, Keycloak as broker (useful early in the transition)**
When you want to leave the existing login experience untouched while attaching new apps to Keycloak first, register SiteMinder as an external IdP using Keycloak's Identity Brokering. Users still see the SiteMinder login screen, but new apps are developed against standard OIDC.
Registering a SAML Identity Provider in Keycloak looks like this:
Register SiteMinder as a SAML IdP via kcadm
/opt/keycloak/bin/kcadm.sh create identity-provider/instances -r enterprise \
-s alias=siteminder-idp \
-s providerId=saml \
-s enabled=true \
-s 'config.entityId=https://keycloak.example.com/realms/enterprise' \
-s 'config.singleSignOnServiceUrl=https://sso.example.com/affwebservices/public/saml2sso' \
-s 'config.nameIDPolicyFormat=urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' \
-s 'config.postBindingResponse=true' \
-s 'config.wantAssertionsSigned=true'
The Reality of Session Bridging
The two IdPs' sessions are separate. SAML brokering "creates a session on the other side without re-authentication" — it does not merge the sessions into one. You must therefore decide the following at design time:
- **Session lifetime alignment**: deliberately match Keycloak SSO Session Idle/Max to the SiteMinder realm's idle/max, or standardize on the shorter of the two.
- **Logout propagation**: logging out on one side leaves the other side's session alive. A practical solution is a logout orchestration page that chains the SiteMinder logout URL with Keycloak's front-channel logout.
- This topic is covered in depth in the next article (hybrid coexistence patterns).
The Proxy Pattern for Header-Based Apps — oauth2-proxy
This is the solution for category C (unmodifiable header-based apps). Reproduce what the Web Agent did — verify authentication, then inject user headers — using [oauth2-proxy](https://oauth2-proxy.github.io/oauth2-proxy/) and nginx.
User --> nginx (auth_request) --> oauth2-proxy --> Keycloak (OIDC)
| |
| auth OK + headers |
v |
Legacy app (receives SM_USER |
headers unchanged — zero <------+
code modification)
The crux of the oauth2-proxy configuration is **translating Keycloak OIDC claims into SiteMinder header names**.
oauth2-proxy.yaml (alpha config)
upstreamConfig:
upstreams:
- id: hr-payroll
path: /
uri: http://hr-payroll.internal:8080
providers:
- id: keycloak-oidc
provider: keycloak-oidc
clientID: legacy-bridge-proxy
clientSecretFile: /secrets/client-secret
oidcConfig:
issuerURL: https://keycloak.example.com/realms/enterprise
emailClaim: email
audienceClaims: [aud]
injectRequestHeaders:
- name: SM_USER # exactly the header name the legacy app expects
values:
- claim: preferred_username
- name: SM_USERGROUPS
values:
- claim: groups # reproduce the caret-joined format via a Keycloak mapper
- name: X-Custom-Empno
values:
- claim: employeeNumber
On the nginx side, wire it up with the auth_request pattern:
server {
listen 443 ssl;
server_name hr-payroll.example.com;
location /oauth2/ {
proxy_pass http://oauth2-proxy:4180;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/start?rd=$scheme://$host$request_uri;
Pass headers returned by oauth2-proxy to the upstream
auth_request_set $sm_user $upstream_http_sm_user;
auth_request_set $sm_groups $upstream_http_sm_usergroups;
proxy_set_header SM_USER $sm_user;
proxy_set_header SM_USERGROUPS $sm_groups;
Any forged same-name headers from outside are unconditionally overwritten above
proxy_pass http://hr-payroll.internal:8080;
}
}
**Security note**: the header injection principles emphasized in the previous article apply unchanged. The legacy app must be network-isolated so it is reachable only through this proxy, and the proxy must overwrite any client-supplied SM_USER-style headers.
If the group claim must match the SiteMinder format (caret-delimited), handle it with a Keycloak Script Mapper or a custom protocol mapper.
// Core logic of a custom Keycloak ProtocolMapper (caret-delimited group string)
@Override
protected void setClaim(IDToken token, ProtocolMapperModel model,
UserSessionModel userSession, KeycloakSession session,
ClientSessionContext ctx) {
String joined = userSession.getUser().getGroupsStream()
.map(GroupModel::getName)
.collect(Collectors.joining("^"));
token.getOtherClaims().put("smUserGroups", joined);
}
Policy Migration — The Object Mapping Table
The reference mapping when translating the SiteMinder object model into Keycloak concepts:
| SiteMinder object | Keycloak counterpart | Notes |
| --- | --- | --- |
| Policy Domain | Realm, or client grouping | Consider separate realms for very large org units |
| Realm (URL area + scheme) | Client + required auth flow | URL protection moves to app/proxy responsibility |
| Rule (resource + action) | Authorization Services resource/scope | Or move to app-level authorization |
| Policy (rule + users) | Role/Group mapping, AuthZ policy | Sync LDAP groups with group-ldap-mapper |
| Response (header injection) | Protocol Mapper (claim injection) | The header inventory is the mapping input |
| Auth Scheme | Authentication Flow | Reproduce step-up via ACR/LoA conditional flows |
| Protection Level | ACR (Authentication Context Class) | Recreates step-up authentication |
| User Store (AD/LDAP) | User Federation (LDAP provider) | Initially keep connecting the existing LDAP |
| Session (idle/max) | SSO Session Idle / Max | Review the need to unify per realm |
| OnAuthAccept event rules | Event Listener SPI / Required Action | Custom SPI development territory |
There is one important shift of perspective. SiteMinder is **URL-centric** (who may access this URL), whereas Keycloak is **client/token-centric** (which claims and roles are issued to this app). If fine-grained URL-level authorization is truly required, move it to Keycloak Authorization Services or to the gateway level (for example, per-path group checks at the proxy) — but pushing authorization down into the application is healthier long term.
LDAP User Federation is the key device of the early coexistence period. If both systems look at the **same user store**, account synchronization problems disappear.
Connect the existing AD/LDAP to Keycloak as User Federation
/opt/keycloak/bin/kcadm.sh create components -r enterprise \
-s name=corp-ldap \
-s providerId=ldap \
-s providerType=org.keycloak.storage.UserStorageProvider \
-s 'config.connectionUrl=["ldaps://ad.example.com:636"]' \
-s 'config.usersDn=["ou=people,dc=example,dc=com"]' \
-s 'config.bindDn=["cn=svc-keycloak,ou=svc,dc=example,dc=com"]' \
-s 'config.editMode=["READ_ONLY"]' \
-s 'config.syncRegistrations=["false"]'
User and Password Migration — Gradual Capture
If you share the same LDAP, there is no password problem. But when SiteMinder uses its own user store, or you intend to retire the LDAP itself as part of this effort, you can end up unable to move password hashes (proprietary hashing, or export forbidden by policy). The technique to use is **gradual password capture**.
1. Pre-migrate user profiles (IDs, emails, attributes) via SCIM ([RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644)) or batch jobs, leaving passwords empty.
2. Deploy a **custom User Storage SPI** in Keycloak. On a user's first Keycloak login, the SPI delegates validation of the entered password to the legacy store (LDAP bind or the SiteMinder authentication API).
3. On success, store that password as a native Keycloak credential (argon2 hash) and switch to local validation from then on.
4. After some months, when the capture rate is high enough (for example 95 percent), instruct the remaining users to reset their passwords and retire the legacy store.
// The heart of the User Storage SPI: legacy validation + capture on first login
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
String rawPassword = input.getChallengeResponse();
boolean legacyOk = legacyAuthClient.validate(user.getUsername(), rawPassword);
if (legacyOk) {
// Capture: store as a local Keycloak credential -> local validation next time
user.credentialManager().updateCredential(
UserCredentialModel.password(rawPassword));
log.infof("Password captured for user=%s", user.getUsername());
}
return legacyOk;
}
This pattern is a proven way to switch stores without triggering a "password reset stampede" among users. Note that during the capture window the availability of the legacy authentication path affects the availability of Keycloak logins, so attach a circuit breaker and monitoring.
Wave-Based Rollout and the Rollback Plan
Wave Design
Wave 0 (pilot) : 2-3 low-risk apps used by the internal IT team. Validate all patterns.
Wave 1 (low risk) : 10-20 apps with low traffic and low business impact.
Wave 2..N (main) : batches by department/domain. 2-6 weeks per wave.
Wave Final (the core): mission-critical apps. Only after ample coexistence track record.
Last: SiteMinder decommission (policy archive, license termination)
Repeat the same checklist for every wave:
1. **Before**: re-verify the target apps' header/session requirements, configure Keycloak clients/proxies, validate in staging
2. **Cutover**: switch DNS/load balancer routing to the new path (proxy or direct OIDC). Apply a canary (only part of the traffic on the new path) where possible
3. **Observe**: monitor authentication success rate, login latency, and helpdesk ticket volume for at least 1-2 weeks
4. **Confirm or roll back**
The Rollback Plan
Deliberately build a structure in which rollback is easy.
- If the substance of the cutover is limited to a **routing change** (neither the app nor the SiteMinder policies are deleted), rollback is a single routing revert.
- Do not delete SiteMinder policies until N weeks after wave confirmation; keep them preserved in a disabled state.
- Agree on numeric rollback triggers in advance. Example: "if the authentication success rate drops more than 2 percentage points below baseline for 30 minutes, roll back automatically."
Script the routing switch/revert (e.g. swapping an nginx upstream)
./switch-route.sh hr-payroll --to keycloak-proxy # cutover
./switch-route.sh hr-payroll --to siteminder # rollback (within 1 minute)
Validation Scenarios — Tests Run on Every Wave
At minimum, automate the following scenarios.
| No. | Scenario | Checkpoint |
| --- | --- | --- |
| T1 | Unauthenticated user accesses a protected URL | Login redirect, return to the original URL |
| T2 | After login, access another app in the same wave | SSO (no re-login) |
| T3 | Cross between a legacy app and a new app | SSO via the bridge, no double login |
| T4 | Header value fidelity | SM_USER etc. identical in format to legacy |
| T5 | Session idle expiry | Re-auth required after expiry, no infinite redirects |
| T6 | Logout | Both sessions terminated |
| T7 | Header forgery attempt | Externally injected SM_USER ignored/overwritten |
| T8 | Group-based authorization | 403 for users outside the entitled group |
| T9 | Step-up authentication | Re-auth/2FA demanded for high-protection apps |
| T10 | Fault injection | Logins continue with one Keycloak node down |
T4 (header fidelity) is especially important. Build a harness that sends the same request to the legacy environment and the new proxy environment and diffs the header dumps — it is reusable on every wave.
Header fidelity diff harness (conceptual example)
curl -s -b "$LEGACY_COOKIE" https://legacy.example.com/echo-headers > legacy.txt
curl -s -b "$KC_COOKIE" https://bridge.example.com/echo-headers > new.txt
diff <(grep '^SM_' legacy.txt | sort) <(grep '^SM_' new.txt | sort)
The Identity Orchestration Option
Instead of building everything yourself, there are commercial layers that absorb the transition complexity. Identity orchestration products such as [Strata Maverics](https://www.strata.io/) place an abstraction layer between SiteMinder and the modern IdP, switch which IdP each app uses by policy, and handle session translation and header injection for you.
- **Pros**: no need to develop the coexistence infrastructure (proxies, bridges, logout synchronization) yourself. Especially powerful in multi-IdP environments (mergers and acquisitions).
- **Cons**: yet another vendor dependency and license cost. To avoid "we escaped SiteMinder only to get locked into the orchestration vendor," plan the orchestration layer as a time-boxed tool.
If your organization has strong engineering capacity and the app patterns are simple (mostly header-based), self-building on oauth2-proxy is sensible; if the patterns are complex and the schedule is tight, adopting an orchestration tool is the rational choice.
A Collection of Common Pitfalls
- **Missing header inventory**: migrating only SM_USER and forgetting custom response headers (the X-Custom-Empno kind) — the most common cause of an app rendering a blank page on cutover day.
- **Group delimiter mismatch**: handing a JSON array or comma-separated values to a parser expecting a caret-delimited string, wiping out all permissions.
- **Redirect loops from session lifetime mismatch**: if the proxy session is alive but the Keycloak SSO session dies first, certain configurations produce infinite redirects. Align token refresh and session lifetimes hierarchically.
- **Encoding**: non-ASCII user attributes (Korean names, for example) encoded differently between legacy (RFC 2047) and new (UTF-8) break downstream parsers. Always include non-ASCII cases in the T4 harness.
- **Cutover judgment without time series**: some problems explode not right after cutover but during periodic work — month-end batches, quarterly closing. Include business cycles in the observation window before confirming a wave.
- **Neglecting the "last 5 percent"**: some apps never get migrated. Set a decommission target date, and — a governance trick that actually works — charge the ongoing cost of any remaining app to its owning department after that date.
- **Missing service accounts and batch jobs**: inventorying only human users and overlooking batch/monitoring bots that mimic header authentication. Separate non-human identities into a dedicated track from the start and move them to the client credentials flow.
- **Unprepared helpdesk**: the surge of inquiries on wave cutover day is a predictable event. Simply distributing screenshots of the new login screen and an FAQ to the helpdesk before the cutover dramatically reduces handling time.
- **Missing audit log retention**: regulated industries must retain authentication audit logs for years. If you do not finalize the retention plan for legacy audit logs (archive location, retrieval procedure) before decommissioning SiteMinder, audit season will be painful.
Closing
A SiteMinder-to-Keycloak migration is not an IdP swap project; it is an **application portfolio modernization program**. Eighty percent of success is decided by the inventory, the classification, and a rollback-capable wave design. Technically, once the two bridges — SAML brokering and oauth2-proxy header translation — are solidly built, the rest is a matter of repeatable operational process.
In the next article, accepting the reality that this transition period lasts for years, we dive deep into **coexistence architecture patterns for legacy WebSSO and modern IAM** — protocol bridges, reverse-proxy header injection, identity orchestration, and the strangler fig — together with a fictional case study of migrating 200 apps on a bank intranet.
References
- [Keycloak official documentation](https://www.keycloak.org/documentation)
- [Keycloak release notes](https://www.keycloak.org/docs/latest/release_notes/index.html)
- [Keycloak Server Administration — Identity Brokering](https://www.keycloak.org/docs/latest/server_admin/index.html)
- [Broadcom SiteMinder Tech Docs](https://techdocs.broadcom.com/siteminder)
- [oauth2-proxy official documentation](https://oauth2-proxy.github.io/oauth2-proxy/)
- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [RFC 7644 — SCIM Protocol](https://datatracker.ietf.org/doc/html/rfc7644)
- [RFC 8693 — OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)
- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)
- [OAuth 2.1 draft (draft-ietf-oauth-v2-1)](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/)
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
- [SAML 2.0 Core specification (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)
- [Strata — Identity Orchestration](https://www.strata.io/)
현재 단락 (1/245)
In the previous article we dissected the architecture of SiteMinder. This article covers the next qu...