Identity Azure

Implementing Entra ID Cross-Tenant Synchronization for Multi-Tenant Organizations

Two companies merge on Monday. By Tuesday the integration team is asked: “Why can’t the acquired engineers see the shared Teams, the wiki, and the deployment runbooks?” The honest answer is that B2B guest invites do not scale, do not self-heal, and do not clean up after themselves. Cross-tenant synchronization (CTS) fixes that. It automatically provisions users from a source tenant into a target tenant as B2B members (not guests), keeps their attributes current, and de-provisions them when they leave. This is the SCIM provisioning engine you already know from app provisioning, pointed at another Entra tenant instead of an app.

This is how I stand CTS up safely: scoped to a pilot group first, attribute mappings validated with on-demand provisioning, de-provisioning behavior understood before anyone trusts it, and the whole thing wired into Conditional Access so the synced members are governed by the home tenant’s controls and not left as an open door.

1. Multi-tenant patterns: when CTS is the right tool

Before configuring anything, be honest about the end state you are building toward.

Pattern Use when CTS fit
Single-tenant consolidation One tenant survives; the other is decommissioned CTS is a bridge, not the destination — migrate identities eventually
Multi-tenant operating model Regulatory/data-residency boundaries force separate tenants permanently CTS is the steady state
M&A holding period Day-1 collaboration needed, final architecture undecided CTS for 12-24 months, then decide
Divestiture Carving a business unit out into its own tenant CTS in reverse during the carve-out window

CTS gives you a synchronized B2B member in the target tenant. That member:

If your end goal is one tenant, do not let CTS become permanent shadow infrastructure. It is excellent as a bridge and dangerous as a forgotten dependency.

2. Cross-tenant access settings: the trust prerequisites

CTS will not move a single object until cross-tenant access settings (XTAP) allow it. There are two sides, and people consistently configure only one.

On the source tenant (where users live), under Outbound access settings for the target organization, enable the toggle that allows users to be synchronized into the target. Without it, the source refuses to be a sync source.

On the target tenant (where users land), three things must be configured under Inbound access settings for the source organization:

  1. Cross-tenant sync — allow (the inbound side of the same switch).
  2. Automatic redemption — so synced members are created without each user clicking a consent prompt. This is the difference between a silent member and a pending guest invite.
  3. Trust settings — decide whether to trust the source tenant’s MFA, compliant device, and Entra hybrid joined device claims.

Trust settings are the part that determines whether your users actually get a good experience.

Trust inbound MFA. If you do not, a synced member who already satisfied MFA in their home tenant gets challenged again by your Conditional Access — registering a second authenticator in a tenant they have no business managing credentials in. Trusting the home tenant’s MFA claim is almost always correct for a genuine merger.

Configure inbound trust on the target tenant with Microsoft Graph PowerShell. Get the existing partner configuration first so you PATCH rather than blow away other settings:

Connect-MgGraph -Scopes "Policy.ReadWrite.CrossTenantAccess"

$sourceTenantId = "11111111-1111-1111-1111-111111111111"  # the tenant whose users you import

# Ensure a partner config exists for the source tenant
$partner = Get-MgPolicyCrossTenantAccessPolicyPartner -CrossTenantAccessPolicyConfigurationPartnerTenantId $sourceTenantId -ErrorAction SilentlyContinue
if (-not $partner) {
    New-MgPolicyCrossTenantAccessPolicyPartner -BodyParameter @{ tenantId = $sourceTenantId } | Out-Null
}

# Trust the source tenant's MFA + compliant/hybrid device claims, and allow inbound sync
$body = @{
    inboundTrust = @{
        isMfaAccepted              = $true
        isCompliantDeviceAccepted  = $true
        isHybridAzureADJoinedDeviceAccepted = $true
    }
    automaticUserConsentSettings = @{
        inboundAllowed = $true   # automatic redemption (no per-user consent prompt)
    }
}

Update-MgPolicyCrossTenantAccessPolicyPartner `
    -CrossTenantAccessPolicyConfigurationPartnerTenantId $sourceTenantId `
    -BodyParameter $body

On the source tenant, enable the outbound sync allowance for the target:

# Run this connected to the SOURCE tenant
$targetTenantId = "22222222-2222-2222-2222-222222222222"

Update-MgPolicyCrossTenantAccessPolicyPartner `
    -CrossTenantAccessPolicyConfigurationPartnerTenantId $targetTenantId `
    -BodyParameter @{
        crossTenantAccessPolicyConfigurationPartnerTenant = $null   # placeholder; see note
    }

For the outbound switch, the cleaner path is the portal: Entra admin center -> External Identities -> Cross-tenant access settings -> Organizational settings -> [target org] -> Cross-tenant sync -> Allow users sync into the tenant. The Graph property is b2bDirectConnectOutbound/crossTenantSync and the schema for the outbound sync toggle is less ergonomic in PowerShell than the inbound side, so I do this one in the UI and keep it auditable through the change record.

3. Configuring the synchronization job on the source tenant

CTS is created on the source tenant as a Cross-tenant synchronization configuration. The flow: create a configuration, add the target tenant, then create a provisioning job with admin credentials that point at the target.

In the portal: Entra admin center -> Cross-tenant synchronization -> Configurations -> New configuration. Give it a name like CTS-to-ContosoCorp. Inside it, go to Provisioning, set the Admin Credentials (the target tenant ID is the SCIM endpoint identity; you authorize with an account that holds the directory sync role in the target), and Test Connection.

The connection test verifies that:

If the test fails, it is almost always one of the three XTAP toggles from step 2, not the credentials.

Member vs guest

In Provisioning -> Mappings -> Provision Microsoft Entra ID Users, the attribute mapping includes userType. The default and correct value for CTS is Member:

Target attribute:  userType
Mapping type:      Constant
Constant value:    Member

This is the single most important setting in the whole job. Member produces a real directory member who behaves like staff. Set it to Guest only if you have a specific reason to keep these users in the external/guest experience (rare for a merger, occasionally right for long-lived vendor relationships).

Attribute mappings that matter

The default mapping moves the essentials. The ones I always review:

Source attribute Target attribute Why review it
userPrincipalName userPrincipalName Becomes the member’s UPN in the target; collisions break the create
mail mail Drives mail routing and some app lookups
displayName displayName First thing people see; keep it clean
userType userType -> Member Member vs guest, as above
objectId (anchor) The matching/source anchor — do not change

The matching identifier (the anchor) is what links a source object to its target counterpart on subsequent syncs. CTS uses the source objectId -> stored on the target B2B member. Leave it alone; changing the anchor mid-flight orphans every previously synced user.

Edit a mapping via Graph if you are codifying this (read the synchronization schema, modify, PUT it back):

# servicePrincipalId = the SP backing the CTS configuration; jobId = the provisioning job
$schema = Get-MgServicePrincipalSynchronizationJobSchema `
    -ServicePrincipalId $spId -SynchronizationJobId $jobId

# Inspect and edit $schema.SynchronizationRules[...].ObjectMappings[...].AttributeMappings,
# then write it back:
Set-MgServicePrincipalSynchronizationJobSchema `
    -ServicePrincipalId $spId -SynchronizationJobId $jobId `
    -BodyParameter $schema

For 95% of deployments, the portal mapping editor is correct and the Graph route is for source-controlled, repeatable tenant builds.

4. Scoping filters and on-demand provisioning before full rollout

Never start a CTS job unscoped. The default scope is “Sync only assigned users and groups,” which is exactly what you want — provision flows only for principals explicitly assigned to the configuration’s enterprise app, or you add a scoping filter on top.

Start with a single pilot group. Assign the group to the configuration (Provisioning -> Users and groups -> Add), then add a scoping filter so only that group’s members are in scope even if assignment widens later:

Mapping:    Provision Microsoft Entra ID Users
Source Object Scope -> Add scoping filter:
    Attribute:  department
    Operator:   EQUALS
    Value:      Platform Engineering

Now validate one user without waiting for the 40-minute provisioning cycle, using on-demand provisioning (Provisioning -> Provision on demand). Pick a real pilot user and run it. You get a per-attribute breakdown: what was read from the source, what the mapping produced, and the exact create/update action taken in the target. This is your dry run.

What I look for in the on-demand result:

Only after on-demand provisioning is clean do I Start provisioning for the whole scoped group.

5. De-provisioning: soft-delete and departed users

This is where teams get burned. CTS de-provisioning is driven by whether a user leaves scope, and the behavior is configurable but unforgiving if you misread it.

A synced member is de-provisioned when it falls out of scope, which happens when:

The default and recommended de-provisioning action is soft delete: the member’s accountEnabled is set to false in the target and the object is moved to deleted objects after Entra’s 30-day window — recoverable, audit-friendly. The alternative, hard delete on de-provision, is available but I rarely enable it during an active merger; you want the 30-day grace period when someone is removed from a group by mistake.

The classic incident: an admin narrows the scoping filter (or someone restructures the pilot group) and hundreds of synced members get de-provisioned in one cycle because they all left scope simultaneously. Always check the provisioning logs after any scope change, and treat group/filter edits on a live CTS job as a change with blast radius.

A subtle one for M&A: when an acquired employee leaves and their source account is disabled, CTS soft-deletes them in the target automatically. That is the whole point — you no longer chase orphaned guests across tenants. But it means the source tenant’s joiner-mover-leaver process is now authoritative for target access. Make sure the source HR-driven lifecycle is healthy before you rely on it.

6. Interaction with Conditional Access, PIM, and home-tenant policy

Synced members authenticate at their home tenant, so the home tenant’s Conditional Access governs sign-in (MFA, device, risk). The target tenant’s Conditional Access can also apply to these members at the point they access target resources — and because they are Member, they are subject to your member-targeted policies, not your guest policies.

Two things to get right:

  1. Do not exclude synced members from MFA by accident. Because you trusted inbound MFA (step 2), a member who did MFA at home satisfies your target MFA requirement without re-prompting. That is correct. But verify your target CA policies do not have a stale “external users” exclusion that now lets these members skip controls they should hit.

  2. PIM works on synced members. Since they are real members in the target, you can make them eligible for Entra roles via Privileged Identity Management in the target tenant. This is genuinely useful in a merger — grant an acquired platform lead eligible (not permanent) access to a target subscription’s RBAC, activated through PIM, governed by the target’s approval workflow, while their identity still lives at home.

A scoped CA policy targeting your synced population, requiring compliant device but trusting the home-tenant device claim:

// Conditional Access is configured in the portal/Graph, not KQL.
// Use this KQL in Log Analytics to confirm synced members are hitting the right policy:
SigninLogs
| where TimeGenerated > ago(7d)
| where UserType == "Member"
| where CrossTenantAccessType == "b2bCollaboration"   // synced B2B members
| summarize SignIns = count() by
    ConditionalAccessStatus,
    Policy = tostring(ConditionalAccessPolicies)
| order by SignIns desc

7. Monitoring provisioning logs and reconciling quarantine

CTS surfaces in the same Provisioning logs as app provisioning (Entra admin center -> Monitoring -> Provisioning logs, or auditLogs/provisioning in Graph). Filter by the CTS application to see every create/update/disable/skip with per-object detail.

The state to watch for is quarantine. If the job hits a high failure rate (commonly: target write permission revoked, XTAP toggle flipped off, or a flood of attribute conflicts), Entra quarantines the job, escalates the retry interval, and eventually stops trying. A quarantined job is silently not syncing — which during a merger means people stop getting access and nobody notices for days.

Check job status via Graph and alert on quarantine:

$status = (Get-MgServicePrincipalSynchronizationJob `
    -ServicePrincipalId $spId -SynchronizationJobId $jobId).Status

$status.Code                    # e.g. Active, Paused, Quarantine
$status.Quarantine.Reason       # why it quarantined, if applicable
$status.SteadyStateFirstAchievedTime

In KQL, alert when CTS provisioning starts failing:

AADProvisioningLogs
| where TimeGenerated > ago(1h)
| where ProvisioningAction in ("create", "update", "delete")
| summarize
    Failures = countif(ResultStatus == "failure"),
    Total = count()
  by SourceSystem = tostring(SourceIdentity.details)
| extend FailureRate = round(100.0 * Failures / Total, 1)
| where FailureRate > 20   // tune; sustained high failure precedes quarantine

To clear a quarantine after fixing the root cause, restart the job (Restart-MgServicePrincipalSynchronizationJob with a SynchronizationJobRestartCriterion), which also lets you force a full resync if the watermark drifted.

8. Operating model for M&A: phased onboarding and decommissioning

CTS is a phase, not a destination. The operating model that has worked for me:

  1. Phase 0 — XTAP + pilot. Trust settings, automatic redemption, one department in scope, on-demand validated. (Steps 2-4.)
  2. Phase 1 — wave onboarding. Expand the scoping filter wave by wave (by department or geography), checking provisioning logs after every expansion. Members get app/group/Teams access in the target through normal target-side governance.
  3. Phase 2 — steady state. Source lifecycle (JML) drives target access automatically. PIM governs privileged access for synced members. Monitoring + quarantine alerts in place.
  4. Phase 3 — decision point (12-24 months). Either CTS becomes the permanent multi-tenant model, or you commit to single-tenant consolidation and migrate identities into the surviving tenant, then decommission CTS.

Decommissioning cleanly matters as much as standing it up. Stopping the job soft-deletes every synced member (they all leave scope at once) — so during consolidation, you migrate the workloads and re-home the identities first, and only then stop CTS, so you are removing shadow members rather than yanking live access.

Enterprise scenario

A fintech (call it the parent) acquired a 600-person payments startup. Regulatory data-residency rules meant the startup’s tenant could not simply be merged into the parent within the integration year — the startup’s customer data had to stay in its own tenant under a separate compliance boundary. The platform team needed Day-1 collaboration (shared GitHub Enterprise via Entra SSO, Teams, internal portals) without standing up 600 guest invites that would rot.

They deployed CTS source = startup tenant, target = parent tenant, members synced into the parent so they could be assigned to the parent’s enterprise apps as first-class users. The constraint that bit them: the parent’s Conditional Access had a long-standing policy that blocked all guest/external users from the finance reporting app. Because CTS creates Member, the synced startup engineers were not caught by that guest-blocking policy — but they also were not supposed to reach the finance app. The “block guests” policy gave a false sense of security.

The fix was to stop relying on userType as a security boundary and instead scope access positively. They created a dynamic security group in the parent for the synced population and converted the finance app policies to explicit member-based targeting plus PIM for any privileged finance role, rather than assuming “guest = blocked, member = allowed.”

// Dynamic membership rule (parent tenant) to fence the synced population
(user.userType -eq "Member") and
(user.creationType -eq "Invitation")   // synced CTS members are invitation-created members

That group became the anchor for “treat as integrated-but-segregated”: included in collaboration apps, excluded from regulated finance and HR apps, and the target of a dedicated CA policy requiring compliant device (trusting the home-tenant device claim via inbound trust). On-demand provisioning caught two UPN collisions in the pilot before they reached production, and a quarantine alert during wave 3 surfaced a revoked admin credential within the hour instead of days. CTS ran for 18 months; when residency rules relaxed, they migrated workloads into the parent, re-homed identities, and only then stopped the job.

Verify

Run these to confirm the deployment is actually working end to end:

# 1. XTAP inbound trust + automatic redemption on the TARGET tenant
(Get-MgPolicyCrossTenantAccessPolicyPartner `
  -CrossTenantAccessPolicyConfigurationPartnerTenantId $sourceTenantId).InboundTrust
# expect isMfaAccepted = True, automaticUserConsentSettings.inboundAllowed = True

# 2. The CTS job is Active, not Quarantine/Paused
(Get-MgServicePrincipalSynchronizationJob `
  -ServicePrincipalId $spId -SynchronizationJobId $jobId).Status.Code

# 3. A pilot user exists in the TARGET as a Member created by invitation
Get-MgUser -Filter "userPrincipalName eq 'pilot_startup.com#EXT#@parent.onmicrosoft.com'" `
  -Property userType,creationType,accountEnabled |
  Select-Object UserPrincipalName, UserType, CreationType, AccountEnabled
# expect UserType = Member, CreationType = Invitation, AccountEnabled = True

Then in the portal: Provisioning logs show recent create/update actions with status Success; on-demand provisioning on an unsynced pilot user creates the member and a re-run reports no change; and a SigninLogs query confirms the synced member authenticated and hit the intended Conditional Access policy with MFA satisfied via home-tenant trust (no second prompt).

Checklist

Entra IDCross-Tenant SyncB2BMulti-TenantProvisioning

Comments

Keep Reading