← Blog

Security

M365 tenant-audit voor je een AI-agent mail laat lezen

Een verkeerd geconfigureerd retention label gaf stilletjes zes jaar HR-bijlagen aan een nieuwe service principal. Dit is de M365-audit die wij draaien voor een agent mail leest.

Jacob Molkenboer· Oprichter · A Brand New Company· 11 jun 2026· 7 min
Leren grootboek met messing slot, messing sleutel aan groen lint, gelakte envelop op ivoren bureau bij raam.

Het was een dinsdag in maart. De IT-lead bij een klant had op tenant-niveau Mail.Read toegekend aan de nieuwe email-triage-agent van een leverancier. Het soort dingetje dat je afvinkt tijdens een onboarding-call van twintig minuten. De agent ging om 14:00 live. Om 14:45 had die 31.000 berichten in zijn vector store getrokken, inclusief het archief van de HR-directeur met ontslagbrieven, contracten en salarisbeoordelingen tot 2019 terug.

Niemand had die HR-mailbox in twee jaar aangeraakt. De reden dat er nog steeds zes jaar aan bijlagen in zat, was een Purview retention label genaamd HR-Permanent, automatisch toegepast op alles dat matchte met een loonstrook-bestandsnaampatroon. Het label deed precies waarvoor het was geconfigureerd. Het probleem was dat de service principal van de agent nu alles kon lezen wat het label stilletjes had bewaard.

We rolden de toegang binnen een uur terug. De leverancier ging akkoord met het wissen van zijn store en we kregen daar schriftelijke bevestiging van. Geen toezichthouder erbij betrokken. Maar de volgende ochtend schreven we de checklist hieronder op en draaiden hem op elke andere M365-tenant die we in beheer hadden. Twaalf tenants sindsdien. Vier daarvan hadden iets waar ze niet van wisten.

De twee dingen waar deze checklist tegen beschermt

Eén: een service principal die meer kan lezen dan de mens die hem uitrolt denkt.

Twee: data waarvan de business denkt dat die weg is, maar die Purview trouw jaren heeft bewaard, klaar om opgezogen te worden door alles met Mail.Read.

Allebei zijn op zichzelf misconfiguraties. Het echte incident gebeurt pas als ze overlappen. Daarom kijkt de audit naar beide kanten tegelijk.

Inventariseer de service principals voor je iets nieuws toekent

Het eerste commando dat je draait op een tenant waar je een agent aan gaat toevoegen, is niet de toekenning. Het is de inventarisatie. Verbind met Graph met read-only scopes en lijst elke applicatie op die al mailrechten heeft.

Connect-MgGraph -Scopes "Application.Read.All","Directory.Read.All"

Get-MgServicePrincipal -All -Filter "servicePrincipalType eq 'Application'" |
  ForEach-Object {
    $sp = $_
    Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id |
      ForEach-Object {
        [PSCustomObject]@{
          App        = $sp.DisplayName
          AppId      = $sp.AppId
          Permission = $_.AppRoleId
          Resource   = $_.ResourceDisplayName
        }
      }
  } | Where-Object { $_.Resource -eq "Microsoft Graph" } |
  Export-Csv -Path .\sp-inventory.csv -NoTypeInformation

Je zoekt twee dingen in die CSV. Elke app die je niet herkent. En elke app met Mail.Read, Mail.ReadWrite, Mail.ReadBasic.All of full_access_as_app zonder bijbehorende RBAC-beperking. Dat laatste detail is de killer: een Graph application permission grant geldt onvoorwaardelijk voor de hele tenant, tenzij je hem hebt gescoped.

Scope mailbox-toegang vooraf, niet achteraf

De Microsoft-control waarmee je Graph-mailrechten beperkt tot specifieke mailboxen heet RBAC for Applications (de moderne vervanging voor de oudere ApplicationAccessPolicy). Zonder dat betekent Mail.Read elke mailbox op de tenant. Mét scope geef je de service principal van de agent leesrecht op precies één mail-enabled security group.

De configuratie zit in Exchange Online, niet in Entra. Dat is mede waarom het wordt gemist. De IT-lead die de app inrichtte in het Entra admin center ziet het niet. Draai dit voordat de consent prompt überhaupt aan je CTO is laten zien.

Connect-ExchangeOnline

# Create a mail-enabled security group with the mailboxes the agent
# is allowed to read.
New-DistributionGroup `
  -Name "agent-triage-allowed" `
  -Type "Security" `
  -PrimarySmtpAddress "agent-triage-allowed@contoso.com"

Add-DistributionGroupMember `
  -Identity "agent-triage-allowed" `
  -Member "support@contoso.com"

# Register the agent's service principal and bind it to a scope
# that resolves only to members of that group.
New-ServicePrincipal -AppId "00000000-0000-0000-0000-000000000000" `
                     -ServiceId ""

New-ManagementScope -Name "agent-triage-scope" `
  -RecipientRestrictionFilter "MemberOfGroup -eq 'CN=agent-triage-allowed,OU=...'"

New-ManagementRoleAssignment `
  -App "" `
  -Role "Application Mail.Read" `
  -CustomResourceScope "agent-triage-scope"

Als de leverancier van de agent weigert binnen een gescopete rol te werken, is dat een signaal. Loop weg of ga onderhandelen. De prijs van 'geef het maar tenant-wide en we lossen het later op' is het incident dat ik hierboven beschreef.

Waarschuwing

Een app-permission grant in Entra staat los van Exchange RBAC. Je kunt de RBAC-scope in Exchange intrekken en de Graph-permission blijft staan. Trek bij het offboarden van een agent altijd allebei in, en audit allebei.

Loop de retention labels langs voor je het verwijderlogboek vertrouwt

De meeste M365-beheerders weten dat retention labels bestaan. Minder mensen weten welke labels automatisch worden toegepast, welke immutability triggeren en hoever terug die auto-apply reikt. Hier begon het HR-incident eigenlijk.

Draai dit in Purview om elk retention label, de auto-apply-regel en de retention-duur eruit te dumpen.

Connect-IPPSSession

Get-RetentionCompliancePolicy |
  ForEach-Object {
    $policy = $_
    Get-RetentionComplianceRule -Policy $policy.Name | ForEach-Object {
      [PSCustomObject]@{
        Policy       = $policy.Name
        Workload     = ($policy.Workload -join ",")
        Rule         = $_.Name
        Duration     = $_.RetentionDuration
        Action       = $_.RetentionComplianceAction
        ContentQuery = $_.ContentMatchQuery
        Enabled      = $policy.Enabled
      }
    }
  } | Export-Csv -Path .\retention-map.csv -NoTypeInformation

Twee kolommen tellen het meest. Duration die op Unlimited staat voor een workload waar Exchange in zit. En ContentQuery die matching op sensitive types gebruikt, zoals SensitiveType:"Netherlands Citizen Service (BSN) Number", of bestandsnaampatronen. Elke agent met mail-leesrecht ziet alles wat die regels hebben bewaard, of de gebruiker nu denkt dat hij het heeft verwijderd of niet.

De fix is zelden om de retention weg te halen. De fix is om de agent zo te scopen dat hij de mailboxen waar die regels op richten niet kan lezen. Marketing- en support-mailboxen zijn meestal prima. HR, finance en het kantoor van de CEO niet.

Zet de audit-log aan voor de eerste week van de agent

Mailbox-auditing staat in M365 standaard aan voor acties van owner, admin en delegate. Maar leesacties op applicatieniveau via Graph landen in een andere stream. Je hebt zowel unified audit log als mailbox audit nodig voor application impersonation, en je hebt ze nodig voordat de agent verbindt.

Set-OrganizationConfig -AuditDisabled $false

Get-Mailbox -ResultSize Unlimited |
  Set-Mailbox -AuditEnabled $true `
              -AuditLogAgeLimit 180 `
              -AuditOwner @{Add="MailItemsAccessed"}

# Then, 48h after the agent goes live, look at what it actually touched.
Search-UnifiedAuditLog `
  -StartDate (Get-Date).AddDays(-2) `
  -EndDate   (Get-Date) `
  -RecordType ExchangeItem `
  -ResultSize 5000 |
  Where-Object { $_.UserIds -like "*agent-triage*" } |
  Select-Object CreationDate, UserIds, Operations, AuditData |
  Export-Csv -Path .\agent-first-week.csv -NoTypeInformation

Het MailItemsAccessed-event vertelt je wat de agent daadwerkelijk heeft geopend, in plaats van waar hij toestemming voor had. Als die twee getallen een orde van grootte uit elkaar liggen, heb je iets belangrijks geleerd over het gedrag van de agent voordat iemand anders dat doet.

Vang de service principal in Conditional Access

Conditional Access voor workload identities wordt nog steeds te weinig gebruikt. De meeste tenants passen CA alleen toe op gebruikers. Een service principal die om 03:00 verbindt vanuit een willekeurige hosting-regio zonder IP-allowlist is precies waarvoor CA werd gebouwd, en toch blokkeert het hem meestal niet. De licentie vereist Entra Workload Identities Premium, een aparte SKU die je niet moet vergeten aan de bestelling toe te voegen.

Conditional Access policy: "Restrict agent service principals"

Assignments
  Workload identities: include
    - agent-triage (object id ...)
    - agent-knowledge-base (object id ...)

Conditions
  Locations: include any location, exclude trusted IPs
  Service principal risk: high

Grant
  Block access

Je vangt niet elk misbruik op deze manier. Je vangt wel de voor de hand liggende gevallen, en 'voor de hand liggend' is het meeste van wat misgaat.

De versie van vijf minuten

Als je maar tijd hebt voor één pass voor de consent prompt, draai dan deze vijf queries.

  1. Lijst elke service principal met een Mail.* Graph-permission. Alles wat je niet herkent, trek je in.
  2. Bevestig dat RBAC for Applications voor elke app op die lijst is gescoped naar een security group. Geen scope betekent tenant-wide.
  3. Trek retention labels op met unlimited duration of queries op sensitive types. Noteer welke mailboxen ze raken.
  4. Controleer dat mailbox audit aan staat met MailItemsAccessed voor owners en admins. Zonder dat heb je geen forensics.
  5. Bevestig dat een Conditional Access policy zich richt op de service principal van de agent. Als je de Workload Identities Premium SKU niet hebt, schrijf dat gat dan op.

Vijf queries. Misschien twintig minuten als je het nog nooit hebt gedaan, vijf de tweede keer. Geen van allen heldhaftig. Het HR-incident waarmee ik opende was opgevangen bij nummer twee.

Takeaway

Een Graph Mail.Read-grant geldt standaard voor de hele tenant. Scope hem met RBAC for Applications voor de consent prompt, niet erna.

De stille faalmodus van agent-incidenten

De reden dat ik steeds op deze checklist terugkom, is dat misconfigureerde agent-rechten van nature stil zijn. De agent werkt. Hij geeft antwoorden. Hij triëert mail. Er gaat niets zichtbaar stuk. Het enige signaal zit in de audit log, en alleen als je die hebt aangezet, en alleen als je kijkt.

Als de faalmodus van een AI-systeem stille overreach is in plaats van zichtbare fout, slaan je gewone ops-instincten niet aan. De agent geeft je wat je vroeg plus meer, en die 'meer' verschijnt nooit in welk dashboard dan ook. Er gaat geen alert af als een service principal 30.000 berichten leest die hij niet nodig had. Het wordt gewoon een regel in een audit log waar niemand op querie't.

De checklist hierboven is hoe wij dat zichtbaar maken.

Wat wij hieruit hebben geautomatiseerd

Toen wij vorig kwartaal de email-triage-agent bouwden voor een Nederlandse logistieke klant, was dit precies waar wij op stuitten: het gat tussen 'de app is uitgerold' en 'de app is gescoped'. Wij hebben de vijf queries hierboven uiteindelijk in een pre-flight script gegoten dat draait voordat er in hun tenant een nieuwe app wordt geconsent, plus een tweede pass die wekelijks opnieuw draait en de delta's in een Teams-kanaal post. Wil je hier een versie van voor je eigen stack, dan is ons werk met AI-agents gebouwd op audits zoals deze.

Eén ding dat je vandaag kunt doen: open het Entra admin center, filter Enterprise Applications op 'API permissions contains Mail' en lees de lijst. Vind je een naam die je niet herkent, dan ben je net aan de audit begonnen.

Kern

Een Graph Mail.Read-grant geldt standaard voor de hele tenant. Scope hem met RBAC for Applications voor de consent prompt, niet erna.

FAQ

Hoe lang duurt deze audit de eerste keer?

Ongeveer twintig minuten op een tenant die je kent, langer als je veel enterprise apps hebt. Na de eerste pass draai je de vijf kernqueries in een minuut of vijf.

Heb ik de Workload Identities Premium SKU nodig?

Voor Conditional Access policies die zich op service principals richten wel. Zonder dwingt de policy niets af. Reken de licentie in als je meer dan één of twee agents wilt draaien.

Wat als de leverancier van de agent alleen met tenant-wide Mail.Read wil werken?

Duw terug. RBAC for Applications is sinds 2022 de officiële manier om Graph-mailtoegang te scopen. Een leverancier die het niet wil gebruiken, vraagt jou om zijn security-schuld te dragen.

Hoe vergroten retention labels de blast radius van een Mail.Read-grant?

Ze bewaren mail waarvan de gebruiker denkt dat die weg is. Een agent met Mail.Read ziet alles wat de labels hebben bewaard, ongeacht wat de gebruiker zelf uit beeld heeft verwijderd.

securityai agentsemail automationautomationintegrationsoperations

Iets bouwen?

Start een project