Out of the box, anyone in any other Microsoft 365 organization can find your people in Teams and send them a chat. Attackers count on it. They set up their own tenant, give the account a name like “IT Support (Internal),” and message your staff, often right after flooding their inbox with junk mail so the offer of help looks timely. Arctic Wolf traced this pattern through more than 400 incidents. The goal is usually a remote-access session, and once someone plays along, Arctic Wolf says attackers can be running their own code in under 15 minutes.
No Teams flaw is involved. It’s a default setting. Most small businesses chat with a handful of outside companies, not every organization on Microsoft 365. Here is how to see what yours allows and close it down.
What controls external chat
Microsoft calls this external access. A few organization-wide settings decide who can reach your users:
- Other Microsoft 365 organizations. Allow all external domains (the default), allow only the domains you list, block the domains you list, or block all of them. Either way it takes two: the other organization has to allow yours too.
- Unmanaged Teams accounts. People using Teams with a personal Microsoft account. These are on by default, and by default those people can start the chat.
- Organizations with only trial licenses. Blocked by default. Arctic Wolf says attackers prefer free and trial tenants because they take almost no setup.
- Blocked users. A list of specific outside people who can’t chat with anyone in your organization. Off by default.
On top of those sit external access policies, which decide which of your users the settings apply to. Everyone gets the org-wide default policy unless you assign a custom one, and a person needs both the organization setting and their policy to allow it. A custom policy can also carry its own domain list, and that list takes precedence over the organization’s.
What you need
- To look, the Teams Reader or Global Reader role. To change anything, Teams Administrator. Microsoft Learn lists the Teams roles.
- For the admin center, any web browser.
- For the script, PowerShell 7 and the MicrosoftTeams module. Microsoft supports it on PowerShell 7 on all platforms, so unlike my retention policy script, this one runs on Linux. Sign in with an account that can read Teams settings, such as a Teams Administrator.
Install-Module MicrosoftTeams -Scope CurrentUser
Check in the Teams admin center
- Sign in to the Teams admin center.
- Go to Users > External access.
- Look at Teams and Skype for Business users in external organizations. If it says Allow all external domains, any organization can reach your users.
- Look at People in my organization can communicate with unmanaged Teams accounts, and the checkbox under it, External users with Teams accounts not managed by an organization can contact users in my organization.
- Select the Policies tab to see the org-wide default policy and any custom ones.
Check with PowerShell
The admin center is fine for a quick look. For a record you can save and check again later, this script reads every setting, says in plain words what each one means, and suggests what to tighten. It changes nothing.
#Requires -Modules MicrosoftTeams
<#
.SYNOPSIS
Checks who outside your organization can chat with your users in Microsoft Teams, and
suggests what to tighten. Read-only.
.DESCRIPTION
Connects to Microsoft Teams PowerShell and reads the external access settings: which other
Microsoft 365 organizations can chat with your users, whether people with personal Teams
accounts can, whether organizations with only trial licenses are blocked, any blocked
external users, and the external access policies that decide which of your users the
settings apply to. It describes each setting in plain words, prints a table with
suggestions, and saves the results to a CSV file. It changes nothing in the tenant.
Works in PowerShell 7 on Windows, Linux and macOS.
.PARAMETER UseDeviceCode
Sign in with a code on another device. Use this on a machine without a browser.
.PARAMETER OutputPath
Where to save the CSV. Defaults to TeamsExternalAccess.csv in the current folder.
.EXAMPLE
./Get-TeamsExternalAccessReport.ps1 -UseDeviceCode
.NOTES
Needs the MicrosoftTeams module and an account that can read Teams settings, such as a
Teams Administrator.
#>
[CmdletBinding()]
param(
[switch]$UseDeviceCode,
[string]$OutputPath = './TeamsExternalAccess.csv'
)
$findings = [System.Collections.Generic.List[object]]::new()
function Add-Finding {
# One row of the report: a setting, its value in plain words, and what to consider changing.
param([string]$Setting, [string]$Value, [string]$Advice = '')
$findings.Add([pscustomobject]@{ Setting = $Setting; Value = $Value; Advice = $Advice })
}
function Get-DomainList {
# Returns the domain names in an allowed or blocked list, whichever form the module uses:
# objects with a Domain property, an allow list with an AllowedDomain property, or text
# such as "Domain=contoso.com".
param($Value)
@(foreach ($item in @($Value | Where-Object { $_ })) {
if ($item.PSObject.Properties['AllowedDomain']) { Get-DomainList -Value $item.AllowedDomain; continue }
if ($item.PSObject.Properties['Domain'] -and $item.Domain) { "$($item.Domain)"; continue }
$text = "$item"
$matched = [regex]::Matches($text, 'Domain=([^\s,;{}]+)')
if ($matched.Count -gt 0) { $matched | ForEach-Object { $_.Groups[1].Value } }
elseif ($text -and $text -notmatch 'AllowAllKnownDomains') { $text }
})
}
function Test-AllowAllDomain {
# True when the allowed list is "allow all known domains", the Teams default.
param($Value)
foreach ($item in @($Value | Where-Object { $null -ne $_ })) {
if ($item.GetType().Name -eq 'AllowAllKnownDomains' -or "$item" -match 'AllowAllKnownDomains') { return $true }
}
$false
}
function Get-OnOff {
# Settings come back as True or False; anything else is reported as on, the Teams default.
param($Value)
if ("$Value" -eq 'False') { 'off' } else { 'on' }
}
$connect = @{ ErrorAction = 'Stop' }
if ($UseDeviceCode) { $connect.UseDeviceAuthentication = $true }
elseif (($IsLinux -or $IsMacOS) -and (Get-Command Connect-MicrosoftTeams).Parameters.ContainsKey('DisableWAM')) {
# The module's default sign-in goes through Web Account Manager, which exists only on
# Windows. Elsewhere it fails looking for kernel32.dll, so ask for the browser sign-in.
$connect.DisableWAM = $true
}
Connect-MicrosoftTeams @connect | Out-Null
try {
$federation = Get-CsTenantFederationConfiguration -ErrorAction Stop
# Other Microsoft 365 organizations. AllowFederatedUsers is the master switch.
$allowed = @(Get-DomainList -Value $federation.AllowedDomains)
$blocked = @(Get-DomainList -Value $federation.BlockedDomains)
$setting = 'Other Microsoft 365 organizations'
if ("$($federation.AllowFederatedUsers)" -eq 'False') {
Add-Finding $setting 'Off for everyone'
}
elseif ($null -eq $federation.AllowedDomains) {
Add-Finding $setting 'Unknown' -Advice "Couldn't read the allowed domains. Check Users > External access in the Teams admin center."
}
elseif (Test-AllowAllDomain -Value $federation.AllowedDomains) {
$value = 'Any organization can chat with your users'
if ($blocked.Count -gt 0) { $value += ", except $($blocked.Count) blocked domain(s): $($blocked -join ', ')" }
Add-Finding $setting $value -Advice 'Allow only the organizations you work with.'
}
elseif ($allowed.Count -eq 0) {
Add-Finding $setting 'None, unless a custom policy allows some'
}
else {
Add-Finding $setting "Only $($allowed.Count) listed domain(s): $($allowed -join ', ')"
}
# People with personal (unmanaged) Teams accounts.
$setting = 'Personal Teams accounts'
if ("$($federation.AllowTeamsConsumer)" -eq 'False') {
Add-Finding $setting 'Off'
}
elseif ("$($federation.AllowTeamsConsumerInbound)" -eq 'False') {
Add-Finding $setting 'On, but only your users can start a chat'
}
else {
Add-Finding $setting 'On, and they can start chats with your users' -Advice 'Turn this off, or at least stop personal accounts from starting chats.'
}
# Organizations that have only trial licenses. Blocked is the default.
$setting = 'Organizations with only trial licenses'
switch ("$($federation.ExternalAccessWithTrialTenants)") {
'Blocked' { Add-Finding $setting 'Blocked' }
'Allowed' { Add-Finding $setting 'Allowed' -Advice 'Set this to Blocked unless you work with an organization that has only trial licenses.' }
default { Add-Finding $setting 'Not reported' -Advice 'Update the MicrosoftTeams module to see this setting.' }
}
# Individual external people the organization has blocked.
try {
$access = Get-CsTeamsExternalAccessConfiguration -ErrorAction Stop
$users = @($access.BlockedUsers | Where-Object { $_ })
if ("$($access.BlockExternalAccessUserAccess)" -eq 'True') { Add-Finding 'Blocked external users' "On, $($users.Count) blocked" }
else { Add-Finding 'Blocked external users' 'Off' }
}
catch {
Write-Warning "Couldn't read the blocked users list: $($_.Exception.Message)"
}
# External access policies decide which of your users the settings above apply to. Global
# covers everyone who doesn't have a custom policy.
foreach ($policy in @(Get-CsExternalAccessPolicy -ErrorAction Stop)) {
$name = "$($policy.Identity)" -replace '^Tag:', ''
if ($name -eq 'Global') { $name = 'Global (everyone without a custom policy)' }
$parts = @("other organizations $(Get-OnOff $policy.EnableFederationAccess)")
$domains = "$($policy.CommunicationWithExternalOrgs)"
if ($domains -and $domains -ne 'OrganizationDefault') { $parts += "domains: $domains" }
$parts += "personal accounts $(Get-OnOff $policy.EnableTeamsConsumerAccess)"
$parts += "personal accounts can start chats $(Get-OnOff $policy.EnableTeamsConsumerInbound)"
$advice = ''
if ($domains -eq 'AllowAllExternalDomains') { $advice = 'Its users can chat with any organization, whatever the organization setting says.' }
Add-Finding "Policy: $name" ($parts -join '; ') -Advice $advice
}
}
finally {
Disconnect-MicrosoftTeams -ErrorAction SilentlyContinue | Out-Null
}
$findings | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8
$findings | Format-Table Setting, Value, Advice -Wrap | Out-String -Width 200 | Write-Host
$todo = @($findings | Where-Object Advice)
if ($todo.Count -gt 0) {
Write-Host "$($todo.Count) setting(s) worth a look. The same table is saved in $OutputPath"
}
else {
Write-Host "Nothing to tighten in these settings. The same table is saved in $OutputPath"
}
Save it as Get-TeamsExternalAccessReport.ps1 and run it. On a machine without a browser, such as an Ubuntu server over SSH, add -UseDeviceCode and sign in from another computer.
./Get-TeamsExternalAccessReport.ps1 -UseDeviceCode
What it does
- Reads the organization settings with
Get-CsTenantFederationConfiguration: the domain list, unmanaged accounts, and organizations with only trial licenses. - Reads the blocked users list with
Get-CsTeamsExternalAccessConfiguration. If that fails, it warns you and carries on. - Lists every external access policy with
Get-CsExternalAccessPolicy, and flags a custom policy that allows every domain, because it overrides the organization’s list for the people who have it. - Prints a table with a suggestion next to each setting worth changing, and saves it to TeamsExternalAccess.csv.
- On a tenant with Microsoft’s defaults, it flags two settings: any organization can chat with your users, and unmanaged accounts can start chats.
What to change
Work down this list. The first item does the most.
- Allow only the organizations you work with. Under Users > External access, choose Allow only specific external domains and add your customers, suppliers, IT provider and accountant. Ask each department who they chat with before you switch, because everyone else gets blocked. Tony Redmond makes the case for an allow list at Office 365 for IT Pros and runs his own tenants that way.
- Turn off unmanaged Teams accounts, or at least clear the box that lets them contact your users. With the box cleared, your people can still start a chat with a contractor on a personal account, but strangers can’t start one with them.
- Leave trial-only organizations blocked. It’s the default. The script tells you if someone changed it.
- Give a team that needs open chat its own policy. If sales has to reach anyone, a custom external access policy can allow more domains for those people while everyone else stays on the allow list.
- Tell people how real IT support reaches them. Name the people and the channel. If your IT provider is another company, its domain goes on the allow list, so anything else claiming to be IT stands out. Anyone asking for a remote session gets a call back on a number you already have, not one they give you.
- Limit the remote-access tools. Arctic Wolf says Quick Assist, which comes with Windows, is the tool these attackers ask for most. If your IT support doesn’t use it, Microsoft suggests you disable or remove it.
Things to know
- A block list is whack-a-mole. Arctic Wolf saw attackers move from the free onmicrosoft.com addresses every tenant gets to cheap domains they register themselves. An allow list doesn’t care how many new domains they buy.
- People from a blocked domain can still join your meetings anonymously if your meeting settings allow it.
- Teams labels outside senders as External and asks your user to accept or block the first message. The fake accounts add “(Internal)” to the name to talk past that label, so teach people to trust the label, not the name.
- With Microsoft Defender for Office 365, people can report a suspicious message right from Teams.
- On Linux, the Teams module’s normal sign-in fails with a kernel32.dll error, because it goes through Web Account Manager, a Windows component. The script adds
-DisableWAMfor you. If you connect yourself, add-DisableWAMor-UseDeviceAuthenticationtoConnect-MicrosoftTeams. - The script only reads settings. It doesn’t change anything, and it disconnects when it finishes.
If your tenant still has Microsoft’s defaults, the script will flag two settings, and changing them takes minutes. The slow part is finding out which outside companies your people really chat with. Start there.