DEV Community

AdminPackStudio
AdminPackStudio

Posted on

A monthly Exchange Online hygiene check - external forwarding and mailbox permissions with read-only Get- cmdlets

A monthly Exchange Online hygiene check: forwarding and permissions, read-only

Two things quietly drift in almost every Exchange Online tenant:

  1. Mail leaving the org automatically. A forward set "temporarily" two years ago, an inbox rule created by an attacker after a phished password, or a transport rule that BCCs a mailbox nobody remembers.
  2. Mailbox access nobody can explain. Full Access on the finance shared mailbox for someone who moved teams, Send As granted to a group that has since doubled in size, or a leaver still listed as a delegate.

Neither shows up as an outage, so neither gets looked at until an audit or an incident. The fix is a boring monthly check that only reads. This post walks through one, using Get- cmdlets from the ExchangeOnlineManagement module. Nothing here changes a mailbox, a rule, or a policy. Any fixes go through your normal change process.

Cmdlet names, parameters, and default policy behavior change over time, so check current Microsoft docs before you rely on any of it.


1. Set up a read-only session

  • Module: ExchangeOnlineManagement (the v3 module). Install it from PSGallery or your internal repo.
  • Role: use a read-only role for this job. In Exchange, the View-Only Organization Management role group is the usual starting point. In Entra, Global Reader also gives read access to Exchange settings. You don't need Exchange Administrator or Global Administrator to run a monthly audit.
  • If a cmdlet "doesn't exist" in your session, your role probably doesn't include it. Exchange Online only loads the cmdlets your roles allow. Ask for the right read role rather than borrowing an admin's session.
Connect-ExchangeOnline -UserPrincipalName auditor@contoso.com -ShowBanner:$false
Get-ConnectionInformation | Select-Object TenantID, UserPrincipalName, State
Enter fullscreen mode Exit fullscreen mode

Check the tenant before you run anything. If you manage more than one tenant, it's easy to audit the wrong one.


2. Forwarding lives in four places

Most people check one and miss three. Here's the map:

Where What to look at Who usually sets it
Mailbox forwarding ForwardingSmtpAddress, ForwardingAddress, DeliverToMailboxAndForward ForwardingSmtpAddress can be set by the user in Outlook on the web or by an admin. ForwardingAddress is admin-set and points at a recipient object, often a mail contact for an external address
Inbox rules ForwardTo, ForwardAsAttachmentTo, RedirectTo (and DeleteMessage) The user, or an attacker signed in as the user
Transport (mail flow) rules RedirectMessageTo, BlindCopyTo, CopyTo, AddToRecipients Admins
Org-level controls Outbound spam policy AutoForwardingMode, remote domain AutoForwardEnabled Admins / security

2a. Mailbox forwarding

$mbx = Get-EXOMailbox -ResultSize Unlimited -Properties ForwardingSmtpAddress, ForwardingAddress, DeliverToMailboxAndForward

$mbx | Where-Object { $_.ForwardingSmtpAddress -or $_.ForwardingAddress } |
  Select-Object DisplayName, PrimarySmtpAddress, RecipientTypeDetails,
                ForwardingSmtpAddress, ForwardingAddress, DeliverToMailboxAndForward
Enter fullscreen mode Exit fullscreen mode

Get-EXOMailbox returns a small default property set, so ask for the forwarding properties by name. ForwardingAddress shows a recipient identity, not an SMTP address. To see whether that recipient is external, look it up:

Get-Recipient -Identity '<value from ForwardingAddress>' |
  Select-Object DisplayName, RecipientTypeDetails, PrimarySmtpAddress, ExternalEmailAddress
Enter fullscreen mode Exit fullscreen mode

A MailContact or MailUser with an external address means mail is leaving your org.

2b. Inbox rules

This one is slow on large tenants because it runs per mailbox. Run it after hours, or start with shared mailboxes and high-value users (execs, finance, payroll, admins).

$targets = Get-EXOMailbox -RecipientTypeDetails UserMailbox, SharedMailbox -ResultSize Unlimited

$ruleHits = foreach ($m in $targets) {
  Get-InboxRule -Mailbox $m.PrimarySmtpAddress -IncludeHidden -ErrorAction SilentlyContinue |
    Where-Object { $_.ForwardTo -or $_.ForwardAsAttachmentTo -or $_.RedirectTo -or $_.DeleteMessage } |
    Select-Object @{n='Mailbox';e={$m.PrimarySmtpAddress}}, Name, Enabled,
                  @{n='ForwardTo';e={$_.ForwardTo -join ';'}},
                  @{n='ForwardAsAttachmentTo';e={$_.ForwardAsAttachmentTo -join ';'}},
                  @{n='RedirectTo';e={$_.RedirectTo -join ';'}},
                  DeleteMessage, MoveToFolder
}
$ruleHits | Format-Table -AutoSize
Enter fullscreen mode Exit fullscreen mode

Things that deserve a second look even when they don't forward:

  • Rules with throwaway names (a single dot, random characters) that delete messages or move them to rarely opened folders such as RSS Feeds or Archive. That's a common way to hide replies from a victim after a compromise.
  • Rules that match words like "invoice", "payment", "wire", or "password".

-IncludeHidden also returns some hidden system rules. The Where-Object filter keeps the output focused on rules that actually forward, redirect, or delete.

2c. Transport rules

Get-TransportRule |
  Where-Object { $_.RedirectMessageTo -or $_.BlindCopyTo -or $_.CopyTo -or $_.AddToRecipients } |
  Select-Object Name, State, Mode, Priority, RedirectMessageTo, BlindCopyTo, CopyTo, AddToRecipients, WhenChanged
Enter fullscreen mode Exit fullscreen mode

Every rule in that list should have an owner and a ticket or change ID behind it. A BCC rule nobody can explain is a finding, not a curiosity.

2d. Org-level controls

Get-HostedOutboundSpamFilterPolicy | Select-Object Name, IsDefault, AutoForwardingMode
Get-RemoteDomain | Select-Object Identity, DomainName, AutoForwardEnabled
Enter fullscreen mode Exit fullscreen mode

How to read AutoForwardingMode:

  • Off blocks automatic external forwarding (inbox rules and mailbox forwarding to external addresses). The sender gets an NDR.
  • On allows it.
  • Automatic is the system-controlled default. According to Microsoft's docs, its behavior can differ between tenants depending on history, and Microsoft recommends setting On or Off explicitly instead. Check the current docs for your tenant.

Remote domains and transport rules can also block forwarding, and the outbound spam policy doesn't affect forwarding between internal users. If your org policy is "no auto-forwarding outside the company", confirm the setting is really Off rather than assuming the default covers you. The Auto forwarded messages report in the admin portals is also worth a monthly look. Its location moves between portals, so check current docs.


3. Mailbox permissions on shared mailboxes

Three permission types, three different cmdlets:

Permission What it allows Where to read it
Full Access Open the mailbox and read everything in it Get-EXOMailboxPermission
Send As Send mail that looks like it came from the mailbox itself Get-EXORecipientPermission
Send on Behalf Send as "User on behalf of Mailbox" GrantSendOnBehalfTo on the mailbox

Start with shared mailboxes. That's where most of the drift is.

$shared = Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited -Properties GrantSendOnBehalfTo

$fullAccess = foreach ($s in $shared) {
  Get-EXOMailboxPermission -Identity $s.PrimarySmtpAddress |
    Where-Object { -not $_.IsInherited -and $_.User -notlike 'NT AUTHORITY\SELF' } |
    Select-Object @{n='Mailbox';e={$s.PrimarySmtpAddress}}, User,
                  @{n='Rights';e={$_.AccessRights -join ','}}, Deny
}

$sendAs = foreach ($s in $shared) {
  Get-EXORecipientPermission -Identity $s.PrimarySmtpAddress |
    Where-Object { $_.Trustee -notlike 'NT AUTHORITY\SELF' } |
    Select-Object @{n='Mailbox';e={$s.PrimarySmtpAddress}}, Trustee, AccessControlType,
                  @{n='Rights';e={$_.AccessRights -join ','}}
}

$sendOnBehalf = $shared | Where-Object { $_.GrantSendOnBehalfTo } |
  Select-Object PrimarySmtpAddress, @{n='SendOnBehalf';e={$_.GrantSendOnBehalfTo -join ';'}}
Enter fullscreen mode Exit fullscreen mode

What to look for:

  • Leavers and movers. Compare the trustee list against your leaver list, or against disabled accounts in Entra. A disabled account with Full Access isn't an immediate risk, but it shows your offboarding process skipped a step.
  • Group grants. If a mail-enabled security group holds Full Access or Send As, the members are the real access list. Read them with Get-DistributionGroupMember -Identity <group>. Also note that Outlook automapping only applies to direct user grants, not group grants. That explains a lot of "I have access but it doesn't show up" tickets.
  • Broad grants. Ten or more Full Access trustees on one mailbox usually means it's being used as a team inbox and should be reviewed. Maybe it should be a Microsoft 365 group, or the list needs a trim.
  • Exchange admin roles. While you're here, list who holds admin role groups: Get-RoleGroupMember -Identity 'Organization Management'. Admin access to Exchange is effectively access to every mailbox.

4. The monthly routine (about an hour)

1. Connect read-only. Confirm the tenant.
2. Export mailbox forwarding, inbox rule hits, transport rule hits, org controls.
3. Export Full Access / Send As / Send on Behalf for shared mailboxes.
4. Compare with last month: new forwards, new trustees, new or changed rules.
5. Open one ticket per finding, with an owner. Fixes go through change control.
6. File this month's exports in an access-controlled location.
Enter fullscreen mode Exit fullscreen mode

Dated folders make the month-over-month comparison easy:

$out = ".\exo-audit\$(Get-Date -Format 'yyyy-MM')"
New-Item -ItemType Directory -Path $out -Force | Out-Null
$fullAccess | Export-Csv "$out\shared-fullaccess.csv" -NoTypeInformation -Encoding UTF8
$sendAs     | Export-Csv "$out\shared-sendas.csv"     -NoTypeInformation -Encoding UTF8
$ruleHits   | Export-Csv "$out\inboxrule-hits.csv"    -NoTypeInformation -Encoding UTF8
Enter fullscreen mode Exit fullscreen mode

These files hold email addresses and access details. Don't put them in public channels, AI chat tools, or vendor tickets. Keep them where your org keeps other audit evidence.

A simple findings worksheet keeps the review honest:

Mailbox / Finding type / Trustee or target / Business justification /
Owner / Decision (keep, remove, investigate) / Ticket / Reviewed date
Enter fullscreen mode Exit fullscreen mode

5. When you find an external forward you can't explain

Treat it as a possible account compromise first, not as a cleanup task:

  1. Don't delete the evidence on impulse. Record the rule or forward exactly as you found it (export or screenshot) before anyone changes it.
  2. Find out who set it and when. Search the unified audit log in Microsoft Purview for inbox rule and mailbox forwarding changes on that mailbox. Mailbox auditing is on by default in Exchange Online, but retention and what gets logged depend on licensing, so check current docs.
  3. Look for other compromise signs: unfamiliar sign-ins, new MFA methods, recent password resets, or the same rule pattern on other mailboxes.
  4. Follow your incident process. Removing the forward, resetting credentials, and revoking sessions are changes. An authorized admin makes them under a ticket.

6. Common mistakes

Mistake What happens Fix
Checking only ForwardingSmtpAddress Inbox rules and transport rules are missed Check all four places in section 2
Assuming the default blocks external forwarding "Automatic" may not mean what you think in your tenant Read AutoForwardingMode and set it explicitly under change control
Auditing with a Global Admin account A routine audit runs with far more power than it needs Use View-Only Organization Management or Global Reader
Ignoring group-based grants The real access list is invisible Expand group membership in the review
Deleting a suspicious rule before recording it Evidence for the investigation is lost Export first, then follow the incident process
No owner on transport rules Nobody can say if a BCC rule is still needed Every rule gets a name, an owner, and a change ID
Exports left in a shared Downloads folder Audit data becomes its own leak Store evidence with access control and a retention period

If you find forwarding tied to a compromised account, sign-in controls matter as much as mail controls. The Entra ID Conditional Access Starter Pack ($29) covers report-only rollout, exclusions, and break-glass patterns: https://cashflow4375.gumroad.com/l/nhuyrc


Want the full Exchange Online hygiene checklist?

The Exchange Online Admin Hygiene Pack ($29) from Admin Pack Studio turns this into a repeatable set of Markdown playbooks: shared vs user vs group mailbox guidance and a shared mailbox checklist, a permissions and external-forward audit with a findings worksheet and risk ranking, five mail-flow triage cards (can't receive, external bounces, delayed mail, shared mailbox missing in Outlook, suspicious forward), a monthly Exchange Online hygiene checklist, and extra admin break/fix cards (Send As denied, transport rule loops, quarantine false positives). It's docs only. There are no scripts in it and nothing that changes your tenant.

Get the pack: https://cashflow4375.gumroad.com/l/exchange-online-admin-hygiene-pack?utm_source=devto&utm_medium=article&utm_campaign=exo_hygiene


Admin Pack Studio. Not affiliated with Microsoft. Exchange Online, Microsoft 365, Entra ID, and Microsoft Purview are Microsoft products. Operational guidance for admins authorized to manage their tenant. Cmdlets, role names, and default policy behavior change, so check current Microsoft documentation. Examples use placeholder names.

Top comments (0)