LabHub

Blog

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

한국어English日本語

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

2. License Cost

3. Lack of Standards and Ecosystem Isolation

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:

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:

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.

# 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.

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

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

Comments

No comments yet.

Sign in to leave a comment