Every Team is a Microsoft 365 Group. That single fact is the whole governance story: the group is the identity, membership, mailbox, SharePoint site, and lifecycle object; Teams is just one of the workloads layered on top. Govern the group and you govern the Team, the OneDrive-backed files, the Planner, and the guest access — all at once. This guide builds the full control plane end to end in Entra ID, PowerShell, and Microsoft Purview.
The governance surface: one group, many workloads
When a user clicks “Create team,” Entra ID provisions a Microsoft 365 Group and Microsoft 365 fans out the connected resources. Understanding that shared backbone tells you exactly where each control lives.
| Concern | Where the control lives | Tooling |
|---|---|---|
| Name format / vocabulary | Entra ID directory settings (group naming policy) | Graph PowerShell |
| Lifecycle / sprawl | Microsoft 365 Groups expiration policy | Entra admin center or Graph |
| Who can create | Entra ID directory settings + a security group | Graph PowerShell |
| Privacy, guest, external sharing | Container sensitivity labels | Microsoft Purview + Graph |
| Guest lifecycle | Access reviews + external collaboration settings | Entra ID Governance |
| Drift / ownerless groups | Audit log + reporting cmdlets | Graph PowerShell |
Licensing reality check: group naming policy, expiration, and creation restriction all require Entra ID P1 for every member of governed groups. Access reviews and lifecycle policies need Entra ID P2 (or the Microsoft Entra ID Governance SKU). Container sensitivity labels are part of Microsoft Purview Information Protection. Confirm entitlements before you promise any of this to a steering committee.
Install the two module families you will use throughout. The Microsoft Graph PowerShell SDK supersedes the deprecated AzureAD and MSOnline modules; the Exchange/Purview connection carries the sensitivity-label cmdlets.
# Graph SDK for directory settings, expiration, groups, access reviews
Install-Module Microsoft.Graph -Scope CurrentUser
# Security & Compliance (Purview) PowerShell for sensitivity labels
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Connect-MgGraph -Scopes @(
"Directory.ReadWrite.All",
"Group.ReadWrite.All",
"Policy.ReadWrite.Authorization",
"AccessReview.ReadWrite.All"
)
Step 1 — Group naming policy: prefixes, suffixes, and blocked words
A naming policy does two things: it stamps a deterministic prefix/suffix onto every new group’s name and alias, and it rejects names containing blocked words. The policy lives in the directory setting template “Group.Unified”. There is no first-class cmdlet, so you instantiate the template and patch its values.
The prefix/suffix string supports fixed text and Entra attribute tokens such as [Department], [Company], and [CountryOrRegion]. A common pattern is a fixed GRP_ prefix plus a department token, so a marketing group becomes GRP_Marketing_<typed name>.
# Get the Group.Unified setting; create from template if it doesn't exist yet
$template = Get-MgBetaDirectorySettingTemplate |
Where-Object { $_.DisplayName -eq "Group.Unified" }
$setting = Get-MgBetaDirectorySetting |
Where-Object { $_.TemplateId -eq $template.Id }
if (-not $setting) {
$values = $template.Values | ForEach-Object {
@{ Name = $_.Name; Value = $_.DefaultValue }
}
$setting = New-MgBetaDirectorySetting -TemplateId $template.Id -Values $values
}
Now set the naming convention and the blocked-word list. Blocked words are a case-insensitive, comma-separated list (up to 5,000 entries) and match the user-supplied portion of the name only — not your own prefix.
$body = $setting.Values
($body | Where-Object Name -eq "PrefixSuffixNamingRequirement").Value =
"GRP_[Department]_[GroupName]"
($body | Where-Object Name -eq "CustomBlockedWordsList").Value =
"CEO,Payroll,HR,Legal,Board,Confidential"
Update-MgBetaDirectorySetting -DirectorySettingId $setting.Id -Values $body
[GroupName]is the literal token that represents whatever the user types. Forgetting it collapses every group name into the same static string — a classic and embarrassing rollout bug. Global Administrators are exempt from both naming enforcement and blocked words, so always validate with a non-admin test account.
Step 2 — Expiration policy and renewal automation
Left alone, groups accumulate forever. The Microsoft 365 Groups expiration policy sets a lifetime (in days); as a group nears expiry, owners are emailed to renew, and any group with activity (Teams messages, SharePoint edits, Outlook traffic) auto-renews without human action. Unrenewed groups are soft-deleted and recoverable for 30 days.
You can scope expiration to All groups, Selected groups, or None. Start with a pilot security group, not the whole tenant.
# 180-day lifetime, renewal notices to the listed admin while piloting,
# scoped to specific groups by ID
$pilot = "11111111-1111-1111-1111-111111111111"
Update-MgGroupLifecyclePolicy `
-GroupLifecyclePolicyId (Get-MgGroupLifecyclePolicy).Id `
-GroupLifetimeInDays 180 `
-ManagedGroupTypes "Selected" `
-AlternateNotificationEmails "groups-governance@contoso.com"
If no lifecycle policy exists yet, create it with New-MgGroupLifecyclePolicy using the same parameters, then add pilot groups to its scope:
$policyId = (Get-MgGroupLifecyclePolicy).Id
New-MgGroupLifecyclePolicyGroup -GroupLifecyclePolicyId $policyId `
-AdditionalProperties @{ "groupId" = $pilot }
The renewal email lands with the group owners. Ownerless groups never get a renewal prompt and will silently expire — which is exactly why Step 8’s ownerless-group cleanup is not optional. The automatic activity-based renewal materially reduces noise: in practice the only groups that hit the renewal email are genuinely dormant ones, which is the signal you want.
Step 3 — Restrict group and Team creation to an approved security group
By default any user can spawn a Team. To gate creation, set EnableGroupCreation to false and point GroupCreationAllowedGroupId at a security group whose members retain the right. This single setting governs creation across Teams, Outlook, Planner, SharePoint, and Stream simultaneously — there is no Teams-specific toggle.
$allowed = Get-MgGroup -Filter "displayName eq 'Team-Creators'"
$body = $setting.Values
($body | Where-Object Name -eq "EnableGroupCreation").Value = "false"
($body | Where-Object Name -eq "GroupCreationAllowedGroupId").Value = $allowed.Id
Update-MgBetaDirectorySetting -DirectorySettingId $setting.Id -Values $body
Restricting creation does not stop end users from requesting a Team — pair it with a request workflow (a Power Automate flow, a ServiceNow catalog item, or Teams templates) so legitimate needs are met in hours, not blocked. Governance that only says “no” gets routed around with personal accounts and shadow IT.
Step 4 — Author container sensitivity labels
Sensitivity labels have two scopes: file/email content, and groups & sites (container labels). Container labels are what govern Teams. A single label can enforce privacy (Public/Private), external (guest) membership, unmanaged-device access via Conditional Access, and external SharePoint sharing.
Connect to Purview PowerShell, then create the label and configure its container settings via AdvancedSettings.
Connect-IPPSSession -UserPrincipalName admin@contoso.com
New-Label -Name "Confidential-Internal" `
-DisplayName "Confidential - Internal Only" `
-Tooltip "No guests, private membership, managed devices only" `
-ContentType "Site, UnifiedGroup"
Set-Label -Identity "Confidential-Internal" -AdvancedSettings @{
BlockGuestUserAccessOnGroupCreation = "true" # no guests can be added
AllowEmailFromGuestUsers = "false"
AllowAccessToGuestUsers = "false"
}
For the SharePoint container behaviors — external sharing scope, default privacy, and unmanaged-device posture — use the dedicated parameters on Set-Label. SiteExternalSharingControlType accepts ExternalUserAndGuestSharing, ExternalUserSharingOnly, ExistingExternalUserSharingOnly, or Disabled.
Set-Label -Identity "Confidential-Internal" `
-SiteExternalSharingControlType "Disabled" `
-SiteAndGroupProtectionEnabled $true
Build a small, opinionated set — three or four labels (for example Public, General, Confidential-Internal, Highly-Confidential) covering the privacy/guest/device matrix. Long label menus cause mislabeling; the goal is a decision a user can make in two seconds.
Step 5 — Publish label policies and enforce them on teams
A label does nothing until a label policy publishes it to users and turns on container labeling at the tenant level. First flip the tenant feature on (this is the step teams most often miss — labels won’t appear on groups otherwise):
Execute-AzureAdLabelSync # syncs labels into Entra ID for container use
Set-SPOTenant -EnableAIPIntegration $true
Then publish with a policy. Setting MandatoryLabelForGroup (via the policy’s advanced settings) forces a label choice at creation time, and a default label closes the gap for users who would otherwise skip it.
New-LabelPolicy -Name "Container-Label-Policy" `
-Labels "Public","General","Confidential-Internal","Highly-Confidential" `
-ExchangeLocation "All"
Set-LabelPolicy -Identity "Container-Label-Policy" -AdvancedSettings @{
DefaultContainerLabel = (Get-Label -Identity "General").Guid
}
Existing teams created before labeling are unlabeled. Backfill them with Graph by patching the assignedLabels property on the group:
$labelId = (Get-Label -Identity "General").Guid
$teams = Get-MgGroup -Filter "groupTypes/any(c:c eq 'Unified')" -All
foreach ($t in $teams) {
Update-MgGroup -GroupId $t.Id -AdditionalProperties @{
assignedLabels = @(@{ labelId = $labelId })
}
}
Label changes on a container are not always instantaneous. A privacy or guest change can take time to propagate to the connected SharePoint site, and changing the label on an existing group does not retroactively remove guests already added under the old policy. Treat backfill as “set the floor going forward,” then run a one-time guest cleanup.
Step 6 — Guest access governance: B2B, Conditional Access, and access reviews
Container labels gate guests at the group boundary; you still govern guests at the tenant boundary. Three layers stack here.
External collaboration settings (B2B): restrict who can invite guests, and block invitations to all but allowlisted domains. Keep this tight — most tenants want member-only or admin-only invitation rights, never “anyone including guests.”
Conditional Access for guests: target the All guest and external users user scope with a policy that requires MFA and compliant access. Guests authenticate against their home tenant, so you cannot enroll their device — require MFA and use session controls rather than device compliance.
Access reviews are the lifecycle control: a recurring campaign where group owners (or named reviewers) re-attest each guest, and unreviewed or denied guests are auto-removed. Create one scoped to all Microsoft 365 Groups that contain guests:
$params = @{
displayName = "Quarterly guest review - all M365 Groups"
descriptionForAdmins = "Owners re-attest external members each quarter"
scope = @{
"@odata.type" = "#microsoft.graph.accessReviewQueryScope"
query = "/groups?\$filter=groupTypes/any(c:c eq 'Unified')"
queryType = "MicrosoftGraph"
# restrict the principals under review to guests only
queryRoot = $null
}
reviewers = @(@{ query = "./owners"; queryType = "MicrosoftGraph" })
settings = @{
recurrence = @{
pattern = @{ type = "absoluteMonthly"; interval = 3 }
range = @{ type = "noEnd"; startDate = "2026-06-01" }
}
defaultDecision = "Deny" # remove if no decision
defaultDecisionEnabled = $true
autoApplyDecisionsEnabled = $true
instanceDurationInDays = 14
}
}
New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $params
Set
defaultDecision = "Deny"andautoApplyDecisionsEnabled = $truetogether, or the review becomes a quarterly box-ticking exercise that removes no one. The point of a review is that inaction revokes access. Give owners 14 days and an escalation reviewer so a single unresponsive owner doesn’t strand a campaign.
Step 7 — Audit, report, and detect drift
Governance is only real if you can prove it. Three queries cover the bulk of operational reporting.
Ownerless and under-owned groups — these break renewal and accountability. Every governed group should have at least two owners:
$risky = foreach ($g in Get-MgGroup -Filter "groupTypes/any(c:c eq 'Unified')" -All) {
$owners = Get-MgGroupOwner -GroupId $g.Id -All
if ($owners.Count -lt 2) {
[pscustomobject]@{
Group = $g.DisplayName
Owners = $owners.Count
Id = $g.Id
}
}
}
$risky | Sort-Object Owners | Export-Csv ./ownerless-groups.csv -NoTypeInformation
Microsoft 365 also ships a built-in ownerless groups policy (configured in the admin center) that automatically emails the most-active members to nominate themselves as owners — enable it so the CSV above trends toward empty.
Label drift — find Unified groups still missing a container label after backfill:
Get-MgGroup -Filter "groupTypes/any(c:c eq 'Unified')" -All `
-Property "id,displayName,assignedLabels" |
Where-Object { -not $_.AssignedLabels } |
Select-Object DisplayName, Id
Activity and usage for dormant-team identification comes from the Microsoft 365 usage reports (Teams activity, Groups activity) in the admin center or via the reports Graph endpoints — pull the Teams team activity detail report to see last-activity dates and message counts, then reconcile against expiration outcomes.
Enterprise scenario
A 40,000-seat manufacturer rolled the creation gate and a mandatory Confidential-Internal label on day one, then flipped expiration to ManagedGroupTypes = "All" with a 180-day lifetime tenant-wide the same week. Six months later, ~600 teams expired in a single night and soft-deleted — including a handful of plant-floor teams whose only “owner” had left the company. The teams had real SharePoint activity, but activity-based auto-renewal never fired: the membership had been provisioned by a SCIM connector into a dynamic group, and dynamic Microsoft 365 Groups are explicitly excluded from the expiration renewal engine. No owner, no dynamic-group renewal, no prompt — just deletion.
The fix had two parts. First, recover inside the 30-day window with Graph rather than the UI, which has no bulk path:
Get-MgDirectoryDeletedItemAsGroup -All |
Where-Object { $_.GroupTypes -contains "Unified" -and $_.DisplayName -like "GRP_Plant_*" } |
ForEach-Object { Restore-MgDirectoryDeletedItem -DirectoryObjectId $_.Id }
Second, stop trusting expiration to police dynamic groups. They moved dynamic and SCIM-fed teams out of the lifecycle scope entirely (ManagedGroupTypes = "Selected", excluding those IDs) and governed their lifecycle through the access-review and ownerless-group policies instead. The durable lesson: expiration is an owner-driven control, so it only works where human owners and standard (assigned) membership exist. Audit membershipRule and owner count before widening ManagedGroupTypes, never after the first deletion wave.
Verify
Confirm each control with a low-privilege test account and direct reads — never trust the admin-center UI alone.
- Naming: as a non-admin, create a group named
testand confirm it materializes asGRP_<dept>_test. Attempt a name containingPayrolland confirm rejection. - Creation restriction: sign in as a user not in
Team-Creatorsand confirm the “Create team” path is blocked; confirm a member of the group can still create. - Expiration: check a pilot group’s
expirationDateTimeis populated:
Get-MgGroup -GroupId $pilot -Property "displayName,expirationDateTime,renewedDateTime" |
Select-Object DisplayName, ExpirationDateTime, RenewedDateTime
- Labels: create a team, confirm the label picker appears and is mandatory, then re-read the group and confirm
assignedLabelsis set. For a “no guests” label, attempt to add a guest and confirm it is blocked. - Access review: confirm the definition is active and scoped to owners as reviewers:
Get-MgIdentityGovernanceAccessReviewDefinition |
Select-Object DisplayName, Status, @{n="Recurs";e={$_.Settings.Recurrence.Pattern.Type}}
Governance rollout checklist
Pitfalls and next steps
The recurring failure mode is treating these as four independent projects. They are one object — the Microsoft 365 Group — viewed through four lenses, and they interact: expiration without owners orphans data; labels without a creation gate get bypassed by self-service; guest reviews without auto-apply remove no one. Sequence the rollout as creation gate → naming → labels → expiration → guest reviews, piloting each on a scoped group before going tenant-wide.
Three traps worth flagging explicitly. First, Global Admins are exempt from naming and creation policies, so admin testing produces false confidence — always validate as a standard user. Second, sensitivity-label and privacy changes propagate to the connected SharePoint site asynchronously and never retroactively evict existing guests, so backfill must be paired with a one-time cleanup. Third, expiration soft-deletes are recoverable for only 30 days; communicate the renewal mechanic loudly before the first wave of notices, or you will field panicked tickets about “deleted” teams.
From here, codify the directory settings and lifecycle/label policies as infrastructure-as-code (the Microsoft365DSC project handles all of these resource types declaratively), wire the drift reports into a scheduled runbook, and feed access-review and audit-log outcomes into your SIEM so policy drift becomes an alert rather than a quarterly surprise.