Exchange Online Protection is on by default in every tenant, which is exactly why most orgs never tune it. The defaults are junk-folder-heavy and admin-only on quarantine — fine for 50 seats, quietly wrong for an enterprise where bulk mail floods inboxes, false positives vanish into admin-only quarantine, and one IP allow entry silently disables your own DMARC. This guide engineers the EOP inbound stack deliberately: connection filtering, the anti-spam policy with correct SCL/BCL handling, ASF, and quarantine policies that let users self-serve without lowering protection.
Everything here drives from the Exchange Online PowerShell V3 module (
ExchangeOnlineManagement). The GUI lives in the Defender portal under Email & collaboration > Policies & rules > Threat policies, but anti-spam config is security configuration — script it so it is reviewable and identical across tenants.
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
1. The EOP inbound pipeline: filtering then verdict actions
Internalize the order before changing anything. EOP is not one filter; it is a sequence of stages, and every setting plugs into a specific one. A message arriving at *.mail.protection.outlook.com traverses:
- Connection filtering — the
HostedConnectionFilterPolicy(one policy, namedDefault). The source IP is checked against your IP Allow List and IP Block List. An allow-list hit stamps SCL -1 (bypass spam filtering); a block-list hit drops the connection. - Anti-malware — payload scanning, independent of spam scoring.
- Anti-spam (content filtering) — the
HostedContentFilterPolicyassigns a Spam Confidence Level (SCL) and a Bulk Complaint Level (BCL), evaluates Advanced Spam Filter (ASF) rules, and reaches one of several verdicts: spam, high-confidence spam, phishing, high-confidence phishing, or bulk. - Verdict action — each verdict maps to an action (move to Junk, quarantine, redirect, delete) and, when quarantined, to a quarantine policy that decides what the end user may do.
| Stage | Object | What it decides |
|---|---|---|
| Connection filter | HostedConnectionFilterPolicy (Default) |
Allow/block by source IP; SCL -1 bypass |
| Anti-spam | HostedContentFilterPolicy + Rule |
SCL/BCL, ASF, per-verdict action |
| Quarantine handling | QuarantinePolicy |
End-user view/release/request-release rights |
| Outbound | HostedOutboundSpamFilterPolicy + Rule |
Send limits, auto-forward control, restricted-sender trigger |
The single most important fact: an SCL of -1 means “skip filtering entirely.” Connection-filter allow entries, and a transport rule that sets SCL -1, both do this. They are not “trust a bit more” — they are “turn anti-spam off for this path.” Treat every SCL -1 source as a deliberate, audited exception.
2. Connection filtering: IP allow/block and the safe-list trap
There is exactly one connection filter policy per tenant — Default. You cannot create more, and it has no recipient scope; it applies to all inbound mail.
# Inspect the live policy first
Get-HostedConnectionFilterPolicy -Identity Default |
Format-List IPAllowList, IPBlockList, EnableSafeList
# Block a noisy /24; allow a specific application relay that must never be filtered
Set-HostedConnectionFilterPolicy -Identity Default `
-IPBlockList @{ Add = '198.51.100.0/24' } `
-IPAllowList @{ Add = '203.0.113.25' }
Three correctness points that bite people:
- Allow = SCL -1. Any IP in
IPAllowListhas spam filtering bypassed for everything it sends, and your own spoof/DMARC checks are skipped for it too. Use it only for application/relay sources you fully control, never for “a partner we trust” — an attacker who routes through an allow-listed relay inherits the bypass. For partner mail, prefer DMARC alignment plus the Tenant Allow/Block List, which override surgically. EnableSafeList $false— keep it off. The safe list is a Microsoft-curated external reputation feed of high-volume senders. Turning it on ($true) tells EOP to skip filtering for those senders, removing a layer of your own judgment. It sounds like a hardening feature; it is the opposite.
# Confirm the safe-list trap is closed
Set-HostedConnectionFilterPolicy -Identity Default -EnableSafeList $false
Do not use the allow list as a shortcut for “stop quarantining this vendor.” It is a blunt, tenant-wide, filtering-off switch. For a single false positive, use a scoped Tenant Allow/Block List entry or fix the sender’s authentication.
3. Anti-spam policy: SCL/BCL, bulk thresholds, and verdict actions
The core. The HostedContentFilterPolicy holds the settings; a paired HostedContentFilterRule binds it to recipients with a priority (lower wins; the built-in Default policy always applies last to anyone unmatched). Build a custom policy rather than editing Default, so you can scope and version it.
New-HostedContentFilterPolicy -Name "AS-Standard" `
-SpamAction Quarantine `
-HighConfidenceSpamAction Quarantine `
-PhishSpamAction Quarantine `
-HighConfidencePhishAction Quarantine `
-BulkSpamAction MoveToJmf `
-BulkThreshold 6 `
-MarkAsSpamBulkMail On `
-QuarantineRetentionPeriod 30 `
-SpamQuarantineTag "AS-SpamSelfRelease" `
-HighConfidenceSpamQuarantineTag "AdminOnlyAccessPolicy" `
-PhishQuarantineTag "AdminOnlyAccessPolicy" `
-HighConfidencePhishQuarantineTag "AdminOnlyAccessPolicy" `
-BulkQuarantineTag "DefaultFullAccessWithNotificationPolicy" `
-InlineSafetyTipsEnabled $true `
-SpamZapEnabled $true `
-PhishZapEnabled $true
New-HostedContentFilterRule -Name "AS-Standard" `
-HostedContentFilterPolicy "AS-Standard" `
-RecipientDomainIs "contoso.com" `
-Priority 0
How the two scores work:
- SCL (Spam Confidence Level) runs
-1(bypass),0/1(not spam),5/6(spam),7/8/9(high-confidence spam). You do not set a numeric SCL threshold in modern EOP — you choose the action per verdict, where the verdict is what the engine derives from SCL plus signals. The verdicts and their valid actions:
| Verdict | Valid actions |
|---|---|
| Spam | MoveToJmf, AddXHeader, ModifySubject, Redirect, Delete, Quarantine |
| High-confidence spam | MoveToJmf, AddXHeader, ModifySubject, Redirect, Delete, Quarantine |
| Phishing | MoveToJmf, Redirect, Quarantine, Delete |
| High-confidence phishing | Quarantine (forced — cannot be downgraded) |
| Bulk | MoveToJmf, AddXHeader, ModifySubject, Redirect, Delete, Quarantine |
- BCL (Bulk Complaint Level) runs
0(none) to9(highest complaint volume).BulkThresholdis the dividing line — BCL greater than the threshold is bulk.6is a sensible enterprise default (4is aggressive and catches marketing newsletters;7-8is permissive), andMarkAsSpamBulkMail Onmakes the bulk verdict actually fire.
The tiering shown above quarantines the dangerous verdicts but sends bulk to a notification-enabled quarantine policy so users self-serve the newsletter they wanted — removing most “where did my email go” tickets without weakening protection.
QuarantineRetentionPeriodmaxes at 30 days and is set on the content filter policy, not the quarantine policy. After it expires, items are purged and unrecoverable — keep it at 30; shorter retention has burned teams whose users return from leave to an empty quarantine.
4. Advanced Spam Filter (ASF) settings
ASF is a set of granular content tests — HTML frames, embedded tags, numeric IPs in URLs, SPF hard-fail, and similar. Each rule has three states: Off, On (apply the action — increase SCL or mark as spam), and Test (take a test action only, to measure impact without affecting delivery). Most are off by default because several are high false-positive.
# Turn select ASF rules to Test mode first to measure impact
Set-HostedContentFilterPolicy -Identity "AS-Standard" `
-MarkAsSpamSpfRecordHardFail Test `
-MarkAsSpamFromAddressAuthFail Test `
-MarkAsSpamNdrBackscatter Test `
-IncreaseScoreWithNumericIps Test `
-IncreaseScoreWithRedirectToOtherPort Test
Guidance from production:
MarkAsSpamSpfRecordHardFailis tempting but redundant and risky now that spoof intelligence and composite authentication handle SPF/DKIM/DMARC holistically. A naive SPF-allhard-fail check flags legitimate forwarded and mailing-list mail. Leave itOff; rely on the anti-phish spoof engine.IncreaseScoreWith*rules (numeric IPs, redirect to other port, remote image links, .biz/.info URLs) only bump the score — they rarely flip a verdict alone but stack with other signals. Run them throughTestfor a week and read the headers before committing.- Always stage ASF in
Testfirst.Teststamps anX-CustomSpamheader so you can quantify what would have been actioned via message trace, with zero user impact. Promoting straight toOnis the fastest way to manufacture false positives.
# After a measurement window, promote only what proved clean
Set-HostedContentFilterPolicy -Identity "AS-Standard" `
-IncreaseScoreWithNumericIps On `
-IncreaseScoreWithRedirectToOtherPort On
5. Spoof and high-confidence phishing handling
A subtle boundary: in modern EOP, spoof intelligence and impersonation protection live in the anti-phishing policy (Set-AntiPhishPolicy), not the content filter. The content filter owns the phishing verdict actions (where phish-classified mail goes); the anti-phish policy owns spoof detection and unauthenticated-sender handling. Both are EOP — just different objects, and that split confuses people who expect one “spam” policy.
In practice, set PhishSpamAction/HighConfidencePhishAction in the content filter (section 3) to control where caught phish lands — quarantine with an admin-only tag, almost always — and set spoof handling in the anti-phish policy:
Set-AntiPhishPolicy -Identity "Office365 AntiPhish Default" `
-EnableSpoofIntelligence $true `
-AuthenticationFailAction Quarantine `
-EnableUnauthenticatedSender $true `
-EnableViaTag $true `
-HighConfidencePhishAction Quarantine
- High-confidence phishing is non-negotiable — EOP forces it to quarantine; you cannot route it to Junk. Pair it with an
AdminOnlyAccessPolicytag so users cannot self-release a confirmed credential-harvesting message. - Fix individual spoof false positives with Tenant Allow/Block List spoof entries scoped to the exact spoofed pair, never by disabling
EnableSpoofIntelligence(which removes protection for everyone the policy covers).
The most common audit finding here is a high-confidence-phish verdict mapped to a quarantine policy that allows end-user release — letting a user click “release” on the message the engine is most confident is an attack. Always bind high-confidence phish and malware to admin-only release.
6. Designing quarantine policies: permissions and end-user release
Quarantine policies are the lever that most reduces help-desk load without lowering protection — and the one almost nobody configures. A quarantine policy controls, per verdict, what an end user can do with a quarantined message: nothing, request release, or release directly, plus preview, allow/block-sender, and delete. Three built-in policies cover the extremes:
| Built-in policy | End-user experience |
|---|---|
AdminOnlyAccessPolicy |
No access; admin release only (malware, high-confidence phish) |
DefaultFullAccessPolicy |
View, preview, release directly, allow/block sender |
DefaultFullAccessWithNotificationPolicy |
Full access plus quarantine notification emails |
The middle ground most enterprises actually want — let users request release but require an admin to approve — needs a custom policy. Build the permission set explicitly with New-QuarantinePermissions, then attach it:
# Request-to-release: user can view/preview and REQUEST, but not release
$perm = New-QuarantinePermissions `
-PermissionToViewHeader $true `
-PermissionToPreview $true `
-PermissionToAllowSender $false `
-PermissionToBlockSender $true `
-PermissionToDelete $true `
-PermissionToRequestRelease $true `
-PermissionToRelease $false
New-QuarantinePolicy -Name "AS-SpamSelfRelease" `
-EndUserQuarantinePermissions $perm `
-ESNEnabled $true
The portal shortcuts map to fixed decimal bitmask values worth memorizing:
- No access =
0(admin-only). - Limited access =
134(view, request release, preview, block sender, delete — but cannot release directly). - Full access =
236(everything including direct release).
PermissionToRequestRelease and PermissionToRelease are mutually exclusive in any sane design — request-to-release means the user asks and an admin approves; full release means the user self-serves. Reserve full release for the lowest-risk verdicts (bulk, ordinary spam), request-to-release for medium risk, and admin-only for malware and high-confidence phish.
7. Quarantine notifications, retention, and per-policy assignment
End-user spam notifications (ESN) are the digest emails telling users “you have N quarantined messages.” In modern EOP this is per quarantine policy via -ESNEnabled $true (the old EnableEndUserSpamNotifications on the content filter is deprecated). The frequency and branding are global:
# Global quarantine notification settings (frequency, branding, custom sender)
Set-QuarantinePolicy -QuarantinePolicyType GlobalQuarantinePolicy `
-EndUserSpamNotificationFrequency 04:00:00 `
-EndUserSpamNotificationCustomFromAddress "quarantine@contoso.com" `
-OrganizationBrandingEnabled $true
EndUserSpamNotificationFrequency accepts 04:00:00 (every 4 hours), 1.00:00:00 (daily), or 7.00:00:00 (weekly). Every-4-hours balances finding legitimate mail before purge against notification fatigue.
Per-policy assignment ties it together: the quarantine policy is referenced by name in the content filter’s *QuarantineTag parameters (section 3). Mapping verdicts to tags is the actual design decision:
| Verdict | Recommended quarantine policy |
|---|---|
| Bulk | DefaultFullAccessWithNotificationPolicy (or self-release) |
| Spam | custom self-release / request-to-release |
| High-confidence spam | AdminOnlyAccessPolicy |
| Phishing | AdminOnlyAccessPolicy |
| High-confidence phishing | AdminOnlyAccessPolicy (forced quarantine) |
# Re-assert the verdict-to-policy mapping after building custom policies
Set-HostedContentFilterPolicy -Identity "AS-Standard" `
-SpamQuarantineTag "AS-SpamSelfRelease" `
-BulkQuarantineTag "DefaultFullAccessWithNotificationPolicy" `
-HighConfidenceSpamQuarantineTag "AdminOnlyAccessPolicy" `
-PhishQuarantineTag "AdminOnlyAccessPolicy" `
-HighConfidencePhishQuarantineTag "AdminOnlyAccessPolicy"
8. Outbound spam policy and restricted-sender remediation
EOP also protects your outbound reputation. A compromised mailbox blasting spam gets your tenant’s shared IPs blocklisted; the HostedOutboundSpamFilterPolicy is the circuit breaker. Set send limits and, critically, control auto-forwarding.
New-HostedOutboundSpamFilterPolicy -Name "OS-Standard" `
-RecipientLimitExternalPerHour 400 `
-RecipientLimitInternalPerHour 800 `
-RecipientLimitPerDay 800 `
-ActionWhenThresholdReached BlockUserForToday `
-AutoForwardingMode Off `
-BccSuspiciousOutboundMail $true `
-BccSuspiciousOutboundAdditionalRecipients "soc-bcc@contoso.com" `
-NotifyOutboundSpam $true `
-NotifyOutboundSpamRecipients "soc-alerts@contoso.com"
New-HostedOutboundSpamFilterRule -Name "OS-Standard" `
-HostedOutboundSpamFilterPolicy "OS-Standard" `
-SenderDomains "contoso.com" `
-Priority 0
Two decisions that matter:
AutoForwardingMode Offdisables automatic external forwarding for scoped users. Attackers set up auto-forward rules to exfiltrate mail after compromise; turning it off org-wide closes that channel. UseAutomatic(system-controlled) only if a business process genuinely needs it.ActionWhenThresholdReached—BlockUserForToday(block sending until midnight UTC),BlockUser(block until an admin lifts it), orAlert.BlockUserForTodaysuits general users;BlockUseris stricter for high-value mailboxes.
A user who trips outbound limits is added to the Restricted entities list and cannot send until remediated. After you reset the credential and clear the cause:
# Find restricted senders, then unblock after remediation
Get-BlockedSenderAddress
Remove-BlockedSenderAddress -SenderAddress "compromised.user@contoso.com"
Removing a restricted sender before fixing the root cause (reset password, revoked sessions, killed the malicious forward/inbox rule) just lets them resume spamming and get re-blocked, with your reputation more damaged. Remediate first, unblock second.
Enterprise scenario
A retail group (~40k mailboxes) onboarded a marketing-automation vendor sending transactional receipts and promotional newsletters from a shared ESP. Within a day the help desk was buried: receipts were landing in quarantine, and because the org had never touched quarantine policies, every spam-verdict message used the default admin-only experience — users could see nothing and just filed tickets. The marketing team’s instinct was to add the ESP’s IPs to the connection-filter IP Allow List, which would have set SCL -1 and disabled spoof/DMARC checks for a shared relay other tenants also use. A textbook way to inherit someone else’s spam.
The constraint: receipts had to reach inboxes today, promotional bulk could tolerate quarantine-with-self-release, and security would not accept a tenant-wide filtering bypass on a shared ESP IP.
The fix was a verdict-aware split. Receipts passed DMARC once the vendor aligned them, so they stopped being quarantined. The promotional stream scored as bulk (BCL above threshold) and was routed to a self-release quarantine policy — users got a 4-hour digest and released the newsletter themselves, no ticket:
# Self-release for bulk only; receipts handled by DMARC alignment, not an allow entry
$bulkPerm = New-QuarantinePermissions `
-PermissionToViewHeader $true -PermissionToPreview $true `
-PermissionToRelease $true -PermissionToRequestRelease $false `
-PermissionToBlockSender $true -PermissionToDelete $true
New-QuarantinePolicy -Name "Marketing-BulkSelfRelease" `
-EndUserQuarantinePermissions $bulkPerm -ESNEnabled $true
Set-HostedContentFilterPolicy -Identity "AS-Standard" `
-BulkThreshold 6 -MarkAsSpamBulkMail On `
-BulkSpamAction Quarantine `
-BulkQuarantineTag "Marketing-BulkSelfRelease"
Within the notification window ticket volume collapsed, security kept full filtering and DMARC on the ESP, and the durable fix — vendor DMARC alignment plus a Tenant Allow/Block List entry for the one stubborn newsletter domain — replaced any temptation to allow-list a shared IP. The lesson: quarantine policy design, not the connection-filter allow list, is the correct lever for “legitimate mail is being held.”
Verify
Confirm the stack is live and correctly scoped before declaring victory.
# 1. Connection filter: lists explicit, safe list OFF
Get-HostedConnectionFilterPolicy -Identity Default |
Format-List IPAllowList, IPBlockList, EnableSafeList
# 2. Anti-spam policy: verdict actions, bulk threshold, quarantine tags
Get-HostedContentFilterPolicy -Identity "AS-Standard" |
Format-List SpamAction, HighConfidenceSpamAction, PhishSpamAction, `
HighConfidencePhishAction, BulkSpamAction, BulkThreshold, `
MarkAsSpamBulkMail, QuarantineRetentionPeriod, *QuarantineTag
# 3. Rule binding and priority (Default is always last)
Get-HostedContentFilterRule | Sort-Object Priority |
Format-Table Name, Priority, State, RecipientDomainIs
# 4. Quarantine policies and the permission bitmask in effect
Get-QuarantinePolicy | Format-Table Name, ESNEnabled, EndUserQuarantinePermissionsValue
# 5. Outbound limits and auto-forward control
Get-HostedOutboundSpamFilterPolicy | Format-List Name, AutoForwardingMode, `
RecipientLimitExternalPerHour, ActionWhenThresholdReached
# 6. Anyone currently restricted from sending
Get-BlockedSenderAddress
Read the message header of a delivered or quarantined sample — headers are the ground truth for what EOP decided:
X-Forefront-Antispam-Report:—SCL=,BCL=,CAT=(verdict, e.g.SPM,HSPM,PHSH,HPHSH,BULK), andSFV=(whereSKN/SKAindicate a bypass — investigate any unexpected skip).X-Microsoft-Antispam:— the authoritativeBCL:value.Authentication-Results:—compauth=and thereasoncode show why spoof passed or failed.X-CustomSpam:— present when an ASF rule fired (yourTest-mode measurements land here).
Trace and inspect quarantine from PowerShell:
# Recent inbound mail with status, last ~2 hours
Get-MessageTraceV2 -RecipientAddress "user@contoso.com" `
-StartDate (Get-Date).AddHours(-2) -EndDate (Get-Date) |
Select-Object Received, SenderAddress, Subject, Status
# List quarantined items and their applied policy / verdict
Get-QuarantineMessage -StartReceivedDate (Get-Date).AddDays(-1) `
-EndReceivedDate (Get-Date) |
Format-Table ReceivedTime, SenderAddress, Type, QuarantineTypes, PolicyName
In the quarantine portal (Defender portal > Review > Quarantine), confirm a low-risk test message (a bulk-classified newsletter) shows the self-release option for a normal user, and a high-confidence-phish test shows no release — that proves your verdict-to-policy mapping is wired correctly.
Checklist
Pitfalls
The failure modes are predictable and mostly self-inflicted. The connection-filter allow list as a convenience switch is the worst: SCL -1, tenant-wide, and it disables your own spoof/DMARC checks for that source — use the Tenant Allow/Block List or fix sender authentication instead. EnableSafeList $true sounds protective and is the opposite; leave it off. Admin-only quarantine on everything (the unconfigured default) generates tickets for every benign newsletter — tier your verdicts. Promoting ASF straight to On manufactures false positives; always pass through Test. Downgrading high-confidence phish is impossible, and giving users release on it is an audit finding. Unblocking a restricted sender before remediating just resumes the spam and erodes your shared-IP reputation further.
Treat the EOP stack as one system: connection filtering decides who is even scored, the anti-spam policy decides the verdict, and the quarantine policy decides who controls the outcome. Source-control the PowerShell above, diff it on every change, and let DMARC alignment and the Tenant Allow/Block List carry your exceptions.
Next steps
Pair this with anti-phishing impersonation tuning and the Tenant Allow/Block List workflow in the Defender for Office 365 companion guide, wire DMARC to p=reject once accurate authentication results are confirmed, and route end-user “Report message” submissions to a SOC mailbox so quarantine tuning is driven by real reports, not guesswork.