Skip to content

Entra

Hybrid identity for the lab: on-prem Active Directory (ad.devhome.cloud) is synced up to Microsoft Entra ID (devhome.cloud, the same tenant/custom domain as Azure). This is the tenant-side foundation that ArgoCD SSO, Rancher SSO, Vault SSO, and vCenter's Entra federation all build on. Entirely on the Microsoft Entra ID Free tier — nothing here needs P1/P2.

See On-Prem Active Directory for everything that happens on-prem (domain controller, security groups, GPOs, per-VM automation) — this page is specifically about the sync bridge up to Entra ID and the app registrations that consume the groups it carries up.

flowchart LR
    subgraph OnPrem["On-prem (dc VM)"]
        AD["Active Directory<br/>ad.devhome.cloud<br/>app-*-admins groups"]
    end
    subgraph Cloud["Microsoft Entra ID<br/>devhome.cloud"]
        Sync["Cloud Sync agent"]
        Entra["Entra ID directory<br/>(read-only synced groups)"]
    end
    AD -- "Password Hash Sync" --> Sync --> Entra
    Entra -- OIDC --> Apps["ArgoCD / Rancher / Vault / vCenter<br/>see their SSO pages"]
    Entra -- "My Apps tiles" --> MyApps["myapps.microsoft.com<br/>see My Apps"]

Microsoft Entra Cloud Sync

Chose Cloud Sync over the classic Entra Connect Sync: lightweight agent, no dedicated sync server/SQL Server, and it's fully supported running directly on the domain controller (no separate member server required) — reasonable tradeoffs for a single-DC homelab forest.

Authentication method is Password Hash Sync — free, no standing infrastructure (unlike Pass-through Authentication agents or ADFS federation servers).

Agent install and registration

cloud_sync_setup.ps1.tftpl (run by a null_resource in terraform/vsphere/compute/ad-dc-post.tf, after the DC's post-promotion reboot) does the whole thing non-interactively:

  1. Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10)) — gMSA creation needs a KDS root key; backdating it skips the ~10h AD replication-convergence wait that a normal effective time would otherwise impose.
  2. Installs the agent silently (ProvisioningAgentSetup.exe /quiet), fetched over SMB directly from the TrueNAS share rather than Terraform's file provisioner — WinRM's file transfer is chunked/base64-encoded and is extremely slow for a binary this size.
  3. Connect-AADCloudSyncAzureAD using a dedicated break-glass account (see below) — this cmdlet takes a plain username/password with no MFA challenge support, so it needs an account excluded from that requirement.
  4. Add-AADCloudSyncGMSA, then Add-AADCloudSyncADDomain -DomainName "ad.devhome.cloud".

Add-AADCloudSyncADDomain fails from a WinRM session, every time

Add-AADCloudSyncADDomain talks to the agent over a local WCF named pipe (net.pipe://localhost/AADConnectProvisioningAgent). Confirmed by direct testing: this pipe is consistently unreachable from a terraform-driven WinRM remote-exec session specifically — a 12-attempt retry loop with a 15s delay never once succeeded — but the exact same Connect-AADCloudSyncAzureAD + Add-AADCloudSyncADDomain pair succeeds immediately when run inside a Scheduled Task (a different session context than a WinRM remote shell). The script creates a one-shot Scheduled Task for just that step, runs it, polls for a result file, and tears the task down — rather than trying to run it directly in the calling session.

The sync configuration itself isn't scriptable

Per Microsoft's own CLI docs, there's no PowerShell/CLI path for creating the actual sync job (OU scope, attribute mapping, enabling Password Hash Sync, starting the sync) — that's portal-only, one-time: Entra admin center → Identity → Hybrid management → Microsoft Entra Connect → Cloud sync → New configuration, picking "Active Directory to Microsoft Entra ID" specifically (not any "Entra ID to AD"/group-writeback option — those need P1; basic AD→Entra sync doesn't).

A portal banner will claim P1 is required — it isn't, for this direction

The Configurations page shows a P1-license banner regardless of which configuration type you're about to create. It only actually applies to the reverse direction (Entra ID → AD / group writeback). Ignore it and proceed with "Active Directory to Microsoft Entra ID" — don't start the P1 trial it offers.

Entra Connect (extract-only) + Seamless SSO

Use case: without this, every domain-joined machine still has to hit an actual sign-in prompt for Entra-backed apps (ArgoCD, Rancher, Vault, vCenter, My Apps) even though the user already authenticated once at Windows logon. Seamless SSO closes that gap — a user signed into a domain-joined machine gets transparently signed into those apps via Kerberos against the on-prem DC, no separate prompt, no separate typed password. It's a UX improvement layered on top of Cloud Sync, not a replacement for it — Cloud Sync still owns directory sync (which users/groups exist, password hashes); Seamless SSO only affects how an already-synced user's browser session gets authenticated.

Getting there needs one PowerShell module (AzureADSSO.psd1) that only ships inside the classic Entra Connect installer (AzureADConnect.msi) — but actually installing that MSI would stand up a second, competing directory-sync engine alongside Cloud Sync, which is explicitly not wanted here. connect_sync_setup.ps1.tftpl resolves that by extracting the MSI's files with msiexec /a (Microsoft's own documented way to unpack an MSI without running any install actions) — no Entra Connect Sync service ever gets installed or started, just the one module file pulled out of the package.

What's still manual after that (needs an interactive Entra Global Admin sign-in with MFA, no unattended path exists): New-AzureADSSOAuthenticationContext, Enable-AzureADSSOForest, Enable-AzureADSSO -Enable $true — run from the same extracted module, one-time per forest.

entra_sso_gpo.ps1.tftpl then rolls out the two client-side settings Microsoft's Seamless SSO quickstart requires, as their own domain-root-linked GPO (User Configuration, not Computer Configuration — per Microsoft's own instructions):

  1. Adds autologon.microsoftazuread-sso.com (not microsoftonline.com — an easy domain to get wrong, and wrong silently fails SSO rather than erroring) to the Intranet zone's Site to Zone Assignment List.
  2. Enables "Allow updates to status bar via script" for the Intranet zone — one of the two settings the quickstart calls for.

The dedicated Graph management app + break-glass admin

Terraform's azuread provider (in terraform/azure/) needs something to authenticate as before it can create anything — that something can't be created by Terraform itself. terraform/vault/scripts/bootstrap-entra-graph-app.ps1 is the one manual, interactive bootstrap step:

  • Creates an app registration + service principal, grants it Application.ReadWrite.All, Group.ReadWrite.All, User.ReadWrite.All, RoleManagement.ReadWrite.Directory, and Domain.Read.All, and admin-consents all of them programmatically (the API equivalent of the portal's "Grant admin consent" button).
  • Idempotent — safe to re-run after adding a new permission to the list; detects the existing app by display name and only adds what's missing, without rotating the existing client secret.
  • Requires the signed-in account to be at least a Privileged Role Administrator — consenting RoleManagement.ReadWrite.Directory is itself a privileged action.
  • Pins -TenantId explicitly. Without it, Connect-MgGraph uses the "common" multi-tenant endpoint, which resolved to an MSA (personal Microsoft account) identity instead of the Entra ID org identity on this particular tenant — a known quirk on individual/personal Azure subscriptions, where the signup account can be both a consumer MSA and a directory member. Directory APIs like Get-MgServicePrincipal reject MSA auth outright.

Output feeds terraform/vault's entra_client_id/entra_client_secret vars, stored at devhome/entra.

Separately, terraform/azure/entra.tf creates a break-glass admin (admin@devhome.cloud, Global Administrator, force_password_change = true) — needed because the day-to-day portal sign-in for this tenant is a Guest-type identity (a personal Microsoft account invited in), and Entra ID hard-blocks Guest users from configuring Cloud Sync regardless of what role they hold. Cloud-only by necessity, not preference: it has to exist in Entra ID before Cloud Sync can be configured, so it can't be an on-prem account waiting to be synced. Once psharma@devhome.cloud is synced and granted admin roles for daily use, this account doubles as the break-glass Global Admin Microsoft recommends every tenant keep anyway.

Terraform-managed Entra ID resources

All in terraform/azure/entra.tf, authenticated via the azuread provider (devhome/entra):

Resource Purpose
data.azuread_group.ad_argocd_admins / ad_rancher_admins / ad_vault_users / ad_vcenter_admins Read-only lookups of the AD-mastered groups Cloud Sync carries up — see On-Prem Active Directory for where these are actually created and have members. Entra ID rejects writes to synced-in groups, which is exactly why these are data sources, not resource blocks — the cloud-only azuread_group.argocd_admins/rancher_admins resources this page used to describe were removed in the 2026-07 consolidation onto this AD-mastered tier.
azuread_application/azuread_service_principal — argocd, rancher, vault, vcenter OIDC app registrations. group_membership_claims = ["SecurityGroup"] on all four; app_role_assignment_required = true on argocd/rancher/vcenter so only assigned groups can sign in at all — Vault deliberately leaves this unset (any tenant user can complete OIDC login; real authorization happens inside Vault via Vault SSO's identity group alias)
azuread_app_role_assignment — one per app Grants the matching AD-synced group the built-in default access role on that app's service principal. Two independent purposes: gates sign-in for argocd/rancher/vcenter, and — regardless of that gating — is what makes an app's tile actually appear on myapps.microsoft.com for that group's members. Confirmed 2026-07-25: Vault's tile didn't appear at all until this assignment was added, even though its service principal already existed.
azuread_user.cloud_sync_agent The Cloud Sync break-glass account (svc-cloudsync@devhome.cloud), Hybrid Identity Administrator role
azuread_user.break_glass_admin The portal break-glass account (admin@devhome.cloud), Global Administrator role
vault_kv_secret_v2.entra_sso Publishes generated client secrets + AD-synced group object IDs (ad_*_admins_group_id/ad_vault_users_group_id) to Vault for downstream consumers — see below

homepage_url and login_url matter, independent of OIDC working correctly

Confirmed 2026-07-25: vCenter's tile failed to launch from My Apps ("App launch failed", generic Entra-side error) purely because azuread_application.vcenter's web block had no homepage_url set — direct navigation to vcsa.devhome.cloud worked fine the whole time, since that never goes through My Apps' launch path at all. See My Apps for the full pattern across all four apps.

Vault paths

Path Owner Consumed by
devhome/entra terraform/vault terraform/azure's azuread provider auth
devhome/entra-sso terraform/azure/entra.tf ArgoCD, Rancher, Vault, vCenter's SCIM provisioning script, cloud_sync_setup.ps1.tftpl (hybrid admin creds) — see their respective pages for how each one consumes it

devhome/entra-sso follows the same "stack writes back a Terraform-generated credential for something else to consume" pattern as devhome/docs-site — see Vault Consumers.

What's manual, permanently

  • Cloud Sync's actual sync job (OU scope, Password Hash Sync toggle, start) — portal-only, no CLI path exists.
  • The Cloud Sync agent installer — no stable direct-download URL; Microsoft gates it behind "Accept terms & download" in the admin center.
  • The dedicated Graph app's initial creation — something has to authenticate as a real human the very first time; bootstrap-entra-graph-app.ps1 automates everything after that one interactive sign-in.