Skip to main content

Running Multiple SSO Connectors on One Cluster

AlmaForge is designed around one SSO connector per cluster. Apply an OIDCConnector or a GithubConnector, and let role templates read a predictable, stable set of traits from that one provider.

When a cluster has two or more active connectors, traits and external identities behave differently. Use this configuration only for the three scenarios below.

Not for production steady state

Running two SSO connectors as co-equal primary identity providers has two important consequences. A user's trait map is replaced in full on every login. A user signing in through a second connector under the same AlmaForge username is refused by default unless you enable spec.allowIdentityMerging on AuthConfig. Both are documented below. They fit the three scenarios in When to run multiple connectors, but not a long-lived production configuration.

TL;DR​

  1. Know the three supported scenarios: dev, migration, disaster recovery (When to run multiple connectors).
  2. Turn on identity merging explicitly on AuthConfig before the second connector sees a login (Enable identity merging).
  3. Understand that traits are replaced on every login, not merged (How traits work).
  4. Understand that external identities merge into one user record, but user accounts do not merge (How user merging works).
  5. Know that there is no default connector. Every caller picks one in the browser, CLI logins included (Choosing a connector at login).

When to run multiple connectors​

The three scenarios where running more than one SSO connector is useful, in order of blast radius:

Dev​

Use a dev cluster to test a new SSO provider alongside the current one. For example, add Okta temporarily to a cluster that already uses Entra ID, test each connector in a separate window, then delete the one you do not want to keep. Every login is deliberate, and neither connector coexists long term.

Migration from one SSO to another​

You are moving your organization from one identity provider to another, for example Google Workspace to Microsoft Entra ID. For a brief cutover window, both connectors are applied at the same time so engineers who have already moved to the new IdP can log in through it, while engineers still on the old IdP are unaffected. Once the cutover is complete, the old connector is removed.

During a migration window, each engineer logs in through one of the two connectors on any given day: either the old one or the new one. They do not ping-pong between them. This keeps the trait map stable for each user and keeps role templates predictable. The moment an individual engineer cuts over to the new IdP, they start logging in through the new connector and stay there.

Disaster recovery​

When the primary provider is down, apply a preconfigured secondary connector of a different kind for the duration of the incident. For example, switch from Google to Microsoft while Google is unavailable. Remove the secondary connector after the primary provider recovers.

The secondary connector in this scenario should be prepared in advance and kept as an un-applied YAML file in the same store as your break-glass procedure. Applying it in the middle of an incident requires an existing admin session or break-glass access:

Terminal
alma apply -f dr-microsoft-connector.yamlalma login

And the rollback is:

Terminal
alma delete oidcconnector/microsoft
Why DR is one of the supported scenarios

Configure and test the backup connector in advance. Store its YAML with your break-glass procedure, and apply it only when needed. Enable spec.allowIdentityMerging before an outage. See Enable identity merging.

Prerequisites​

Before you run more than one connector at the same time, make sure you have:

  • At least one SSO connector already applied and known to work end to end. Every page under the SSO integrations section is a valid starting point.
  • The YAML for every additional connector you plan to apply, already tested against its own provider in isolation. This page does not teach you how to set up a new connector. It teaches you how to run more than one at once.
  • Cluster-admin rights on AlmaForge, because you will be editing AuthConfig and applying connector resources.
  • A current, working break-glass access procedure. See Break-glass access. Test identity merging before an outage. It is disabled by default.

Enable identity merging​

With the default AuthConfig, a second login attempt for the same AlmaForge username through a different connector is refused. The auth server returns cannot merge identities: allow_identity_merging is disabled.

To allow one user to sign in through more than one connector, set spec.allowIdentityMerging: true on AuthConfig:

Terminal
alma edit authconfig/cluster

identity-merging.yaml:

examples/sso/identity-merging.yaml
# Auth configuration for a temporary multi-SSO migration or recovery window.
apiVersion: almaforge.com/v1
kind: AuthConfig
metadata:
name: cluster
spec:
allowIdentityMerging: true

The field is a tri-state boolean: unset (the default, which is equivalent to false), explicitly false, or explicitly true. Only explicit true enables merging. Once flipped on, the change is effective immediately and applies to every future login.

The flag has two important properties:

  • It is a cluster-wide switch. You cannot turn identity merging on for one connector and off for another. The gate is at the auth-preference level, not the connector level.
  • It does not retroactively touch existing user records. A user who logged in once through a connector before you flipped the flag still has one identity in their spec.identities list. The next login through a different connector will succeed and will append the new identity to that list.

How traits work​

When a user signs in through an OIDC connector, AlmaForge stores supported ID-token claims as traits under their original names. GitHub supplies logins and teams traits from its user and team lookups. The connector name is recorded with the external identity, not added to each trait key.

For example, both Microsoft and Keycloak can provide a groups trait. Its values come from the provider used for that login. A role must interpret those values consistently or be assigned only by the connector whose claims it expects.

On a cluster with a Microsoft connector, a user record after one login looks like this:

Terminal
alma get user/alice@example.com -o yaml
YAML
kind: User
apiVersion: almaforge.com/v1
metadata:
name: alice@example.com
labels:
internal/expires: "1775921955"
spec:
createdBy:
connector:
id: microsoft
identity: alice@example.com
subject: 8f14e45f-ea6a-4c1d-9b2e-7d3a0c5b1f92
type: OIDC
createdAt: "2026-04-10T15:39:15Z"
user:
name: system
identities:
- id: microsoft
identity: alice@example.com
subject: 8f14e45f-ea6a-4c1d-9b2e-7d3a0c5b1f92
type: OIDC
roles:
- admin
traits:
groups:
- 44d56e07-85fc-4ab8-9146-738c259a401c
- 7907db2a-2e6b-46c6-ae9b-91dbaddb156c
name:
- Alice Example
preferred_username:
- alice@example.com

The record shows three important details:

  • metadata.labels["internal/expires"] is the expiry in Unix seconds. User records are temporary and refreshed on each login. Their lifetime follows the requested certificate lifetime, capped by the assigned roles and cluster certificate limit. After expiry, the next sign-in creates a fresh record. Keep durable assignments in the IdP and connector. See SSO user records.
  • spec.identities lists every external identity the user has signed in through during the lifetime of this record, subject to allowIdentityMerging. See How user merging works.
  • spec.traits is a flat map keyed by claim name. Role templates read those values through external.

Traits are replaced on every login​

Every SSO login replaces the trait map in full. When a user logs in through a different connector than the one they used last time, AlmaForge overwrites the user's trait map with the new connector's traits and drops everything from the old connector. The identities list accumulates. The traits do not.

Concretely:

  1. alice@example.com signs in through google. The user record has identities: [google] and traits such as groups, email, and name from Google.
  2. Later, alice@example.com signs in through microsoft. The user record has identities: [google, microsoft] and traits such as groups, preferred_username, and name from Microsoft.
  3. The stored groups values now come from Microsoft. Claims not returned by Microsoft, such as email in this example, are absent. Signing in through Google again replaces the map with Google's claims.

A certificate carries the traits from the connector used to issue it. A later login replaces the stored user traits but does not rewrite existing certificates. Role templates that reference external["<claim>"] need that claim in the certificate used for the connection.

Role templates against claims​

Role templates read traits through the external variable. The bare dot form works for plain identifier names:

role-template-dot.yaml:

examples/sso/role-template-dot.yaml
# Role template that maps a plain OIDC claim to the SSH login.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: dev
spec:
allow:
serverLogins:
- '{{external.preferred_username}}'

Bracket syntax also works. It is required when the claim name contains characters such as /:

role-template-bracket.yaml:

examples/sso/role-template-bracket.yaml
# Role template for OIDC claim names that require bracket syntax.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: dev
spec:
allow:
serverLogins:
- '{{external["preferred_username"]}}'
serverLabels:
team: '{{external["groups"]}}'

The bracket form uses the exact claim name supplied by the IdP.

In a multi-SSO setup, a role template that is meant to cover users from two connectors has to choose one of two shapes:

  • Use shared claim semantics. Both providers emit the same claim names and values for the permissions the role needs. Test that an equivalent user receives equivalent access through either connector.
  • Use one role per connector. Each role references its provider's traits, and that connector's claimsToRoles assigns it. Keep the intended permissions consistent across the roles and test each login path during the migration. See external trait templates.

How user merging works​

User merging adds external identities to one user record. It does not combine separate user accounts.

A user record in the backend is keyed by the AlmaForge username. That username is derived from a claim in the SSO response, typically the email for OIDC or the GitHub username for GitHub. If two different connectors return the same username claim for the same human, both map to the same AlmaForge user record.

At login time, the auth server looks at the existing user record (if any) and compares its spec.identities against the incoming connector. Each identity in the list is a three-field tuple:

  • type: connector type ("OIDC" or "Github").
  • id: the connector's metadata.name.
  • identity: the external username returned by that connector.

With allowIdentityMerging: false (the default)​

The auth server requires the existing user to have exactly one identity, and that one identity must have the same type, id, and identity as the incoming login. If either condition fails, the login is refused with cannot merge identities: allow_identity_merging is disabled.

While the existing user record remains, the user must sign in through that one connector. A second connector can exist on the cluster, but a login under the same username cannot merge into the record. Applying a second connector is harmless until the first user from the second connector tries to log in under a username that already exists, at which point the login fails. This is the default because it prevents a misconfigured second connector from silently rewriting existing user records.

With allowIdentityMerging: true​

The auth server walks the existing user's identities list looking for a match on type + id. If found, the existing identity entry is updated in place (the identity field is refreshed to whatever the new login returned). If not found, a new identity is appended. Either way, the login succeeds and the user's spec.identities list grows to reflect the set of external accounts they have signed in through.

The trait map, on the other hand, is still replaced in full. The identities list accumulates. Traits do not. There is no way to reverse this because it is a property of how the auth server writes the user record at the end of every login, not an option you can turn on and off separately.

What merging does not do​

  • It does not reconcile usernames. Two connectors that return different usernames for the same human create two separate user records. AlmaForge has no way to know they are the same person.
  • It does not merge roles. The user's spec.roles comes from the connector's claimsToRoles mapping on the most recent login, and is replaced on every login alongside the traits. A user who is admin through Google and access through Microsoft will be admin right after a Google login and access right after a Microsoft login.
  • It does not change the certificate. An engineer holding a valid AlmaForge certificate issued through one connector does not automatically pick up capabilities from a second connector. They have to log out and log in again through the other connector for that login's traits and roles to take effect.

Choosing a connector at login​

There is no default connector and no way to mark one. The caller chooses among the active connectors in a browser.

With one active connector, the sign-in page goes straight to it. The page is shown instead after signing out, after the identity provider refuses a sign-in, and on a cluster with a message of the day.

The sign-in page renders one button per connector, labeled with the connector's spec.display if it has one and its metadata.name otherwise. That label is the only control you have over the choice, so give each connector a display name that makes it unambiguous.

A CLI login lands on the same page:

Terminal
alma login --proxy=almaforge.example.com:443

alma prints a sign-in URL and a one-time code, and opens a browser at that page unless you pass --no-browser. Where there is more than one connector, the user picks there, and nothing about the choice is recorded in the local profile, so the next login asks again.

Scripts and CI do not use this path at all. They authenticate through workload identity, where the join method takes the place of a connector.

Operations​

Rolling a migration forward​

Use this sequence to migrate from one SSO provider to another:

  1. Apply and test the new connector. With identity merging disabled, a username already bound to the old connector cannot sign in through the new one. New usernames can still sign in when their claims map to a role, so restrict the new IdP app to the intended users.
  2. Flip allowIdentityMerging: true on AuthConfig. Every existing user's next login through the new connector will now append a second identity to their record rather than failing.
  3. Tell engineers to log in through the new connector, and give it a spec.display that makes it the obvious button on the sign-in page.
  4. Watch the auth audit log for UserLogin events with connector_id: <new-connector-name>. Stragglers will show up as the last few UserLogin events still coming from the old connector.
  5. When the last straggler has moved, delete the old connector. Existing sessions issued through the old connector stay valid until their certificates expire. No new sessions will be issued through it.
  6. Keep identity merging enabled while any user record contains multiple identities. Disabling it at that point rejects that user's next login, even through the remaining connector. Before restoring the default, inspect the user records and use your tested break-glass procedure to plan the transition.

Rolling a DR cutover forward and back​

Use this sequence for a DR cutover:

  1. Your primary connector's provider is unreachable. Users cannot log in.
  2. Apply the pre-prepared secondary connector (see Disaster recovery). If your cluster does not already have allowIdentityMerging: true, edit AuthConfig in the same window to turn it on. Do this from a machine that already has a valid AlmaForge certificate, because your break-glass procedure is the only way in if your certificate has also expired.
  3. Tell engineers to sign in through the secondary connector for the duration of the incident. It is the second button on the sign-in page, for CLI logins as well as Web UI ones, so give it a spec.display that says so.
  4. When the primary recovers, delete the secondary. Existing sessions on it stay valid until expiry, and new logins go back to the primary.

Removal​

Delete an additional connector the same way as any other:

Terminal
alma delete oidcconnector/<name>

Existing sessions issued through the deleted connector remain valid until their certificates expire. No new sessions will be issued.

Removing a connector removes its button from the sign-in page. Anyone who was using it picks a different one on their next login, with no local state to clear first.

Audit trail​

Every login emits a UserLogin event whose attributes carry connector_id and connector_type. Filter the Web UI's audit log by the UserLogin type to see which connector a user is actually using, or list these events from the CLI:

Terminal
alma audit --type=UserLogin --last=1h -o json

Use --follow to keep watching for new logins. See the CLI reference for date and output options.

If you see events from a connector you thought you had removed, the connector resource is still in the cluster state store. Confirm with alma get oidcconnector,githubconnector.

Limitations​

The three scenarios above are the supported use cases. The limitations below explain why multi-SSO is not a production steady-state configuration:

  • Trait-map replacement. The stored traits come from the latest login. Issued certificates retain the traits from their own login. A shared role can grant different access if providers emit different values for the same claim. Verify each connector's mapping and templates before admitting its users.
  • Identity-merging collisions. A second connector that happens to return the same email as the first one will attempt to merge onto the existing user record. If allowIdentityMerging is off, the second login fails. If it is on, the user's identities list grows and the user's traits and roles get rewritten every time they switch connectors. For production, pick one connector.
  • Role mapping drift. Each connector has its own claimsToRoles block. A human who is admin in Microsoft and access in Google will flip roles every time they switch connectors, because roles are replaced on every login alongside traits. For production, pick one connector.
  • No cross-connector policy. AlmaForge does not evaluate both connectors at the same time and give the user the union of their claims. There is no multi-IdP attribute merging or cross-provider assertion composition. The login is a single event with a single source of truth, and the most recent event wins.

For production steady state, run one SSO connector. Use the three scenarios above as transient states, not as permanent configurations.

References​