Skip to content
Published on

From SiteMinder to Keycloak — A Legacy SSO Migration Strategy and Practical Roadmap

Authors

Introduction — 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 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 and RFC 9700 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

CategoryCharacteristicsMigration difficultyApproach
A. Standard-protocol appsAlready integrated via SAML/OIDC (federation partners)LowJust repoint the IdP endpoint to Keycloak
B. Header-based apps (modifiable)Depend on SM_USER headers, source can be changedMediumRewrite as OIDC clients, or proxy
C. Header-based apps (unmodifiable)Vendor gone, no source, change-frozenHighReproduce headers via proxy pattern (no code change)
D. Agent-API-dependent appsCall the SiteMinder SDK/Agent API directlyHighestCase-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

AspectBig bangPhased (recommended)
DurationShort (in theory)Long (1-3 years)
RiskPossible enterprise-wide simultaneous outageLocalized per wave
RollbackPractically impossiblePossible per wave
Coexistence infrastructureNot neededSAML bridges/proxies required
Suitable forSmall estates under ~30 appsBanks/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 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 objectKeycloak counterpartNotes
Policy DomainRealm, or client groupingConsider separate realms for very large org units
Realm (URL area + scheme)Client + required auth flowURL protection moves to app/proxy responsibility
Rule (resource + action)Authorization Services resource/scopeOr move to app-level authorization
Policy (rule + users)Role/Group mapping, AuthZ policySync LDAP groups with group-ldap-mapper
Response (header injection)Protocol Mapper (claim injection)The header inventory is the mapping input
Auth SchemeAuthentication FlowReproduce step-up via ACR/LoA conditional flows
Protection LevelACR (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 / MaxReview the need to unify per realm
OnAuthAccept event rulesEvent Listener SPI / Required ActionCustom 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) 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.ScenarioCheckpoint
T1Unauthenticated user accesses a protected URLLogin redirect, return to the original URL
T2After login, access another app in the same waveSSO (no re-login)
T3Cross between a legacy app and a new appSSO via the bridge, no double login
T4Header value fidelitySM_USER etc. identical in format to legacy
T5Session idle expiryRe-auth required after expiry, no infinite redirects
T6LogoutBoth sessions terminated
T7Header forgery attemptExternally injected SM_USER ignored/overwritten
T8Group-based authorization403 for users outside the entitled group
T9Step-up authenticationRe-auth/2FA demanded for high-protection apps
T10Fault injectionLogins 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 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