Skip to content

Security: shurugiken/secure-ai-microsoft365

Security

docs/security.md

Security Architecture

This document explains the security controls applied throughout this repository. Every pattern here is designed to be auditable, least-privilege, and repeatable across any Microsoft 365 tenant — not just the examples shown in the code.


Table of Contents

  1. Least-Privilege App Registration
  2. Scoping Application Permissions to Specific Mailboxes
  3. Secrets Management
  4. Conditional Access and Audit Logging
  5. Data Protection When AI Touches Sensitive Data
  6. NIST SP 800-171 / CMMC Control-Family Mapping

1. Least-Privilege App Registration

Request only what the app actually needs

Microsoft Graph permissions are not all equivalent. A permission like Mail.ReadWrite.All gives an application the ability to modify every mailbox in the tenant. Requesting broad permissions "just in case" is a security anti-pattern that violates the principle of least privilege.

Rule: start with the narrowest permission that satisfies the use case and widen only with documented justification.

Use case Correct permission Do NOT use
Read email only Mail.Read Mail.ReadWrite, Mail.ReadWrite.All
Read calendar events Calendars.Read Calendars.ReadWrite
Send mail on behalf of a service account Mail.Send Mail.ReadWrite.All
Read user profile User.Read User.ReadWrite.All, Directory.ReadWrite.All
Read group membership GroupMember.Read.All Group.ReadWrite.All

Full permission reference: Microsoft Graph permissions reference

Application vs. Delegated permissions

Type When to use Risk profile
Delegated A signed-in user is present; the app acts on their behalf and is bounded by what that user can access Lower — scoped to one user's data
Application Background services, daemons, automation with no interactive user Higher — accesses data across the tenant without a user context

Prefer delegated permissions wherever a signed-in user flow is possible. Use application permissions only for headless automation, and always pair them with mailbox-scope restrictions (see Section 2).

Admin consent

Application permissions always require admin consent. Delegated permissions that access other users' data (e.g., Mail.Read without the /me/ prefix) also require admin consent. Consent must be granted by a Global Administrator or Privileged Role Administrator before the app can be used.

Never ship code that prompts end-users to consent to application-level permissions.

Example: registering a read-only mail app in Entra ID

# Connect to Microsoft Graph with the required scope
Connect-MgGraph -Scopes "Application.ReadWrite.All"

# Create the app registration
$app = New-MgApplication -DisplayName "Contoso Mail Reader" `
    -SignInAudience "AzureADMyOrg"

# Add ONLY Mail.Read (application permission)
# Role ID for Mail.Read (application): 810c84a8-4a9e-49e6-bf7d-12d183f40d01
$resourceAppId = "00000003-0000-0000-c000-000000000000"  # Microsoft Graph
$mailReadRoleId = "810c84a8-4a9e-49e6-bf7d-12d183f40d01"

$requiredAccess = @{
    ResourceAppId  = $resourceAppId
    ResourceAccess = @(
        @{ Id = $mailReadRoleId; Type = "Role" }  # "Role" = application permission
    )
}
Update-MgApplication -ApplicationId $app.Id `
    -RequiredResourceAccess @($requiredAccess)

Permission type values: "Role" = application permission, "Scope" = delegated permission.


2. Scoping Application Permissions to Specific Mailboxes

The problem with application permissions

When you grant Mail.Read as an application permission, the app can — by default — read every mailbox in the tenant. A compromised credential or a buggy app could exfiltrate the entire organization's email.

Solution A: Exchange Online RBAC for Applications (current approach)

Microsoft replaced Application Access Policies with Role Based Access Control for Applications in Exchange Online. This is the current, supported mechanism.

The approach creates a scoped management role assignment so the app's service principal can only access a specific mailbox or mail-enabled security group.

# Step 1 — Connect to Exchange Online
Connect-ExchangeOnline -UserPrincipalName admin@contoso.onmicrosoft.com

# Step 2 — Register the Entra service principal in Exchange Online
# ObjectId = the Enterprise Application (service principal) object ID from Entra
New-ServicePrincipal -AppId "<CLIENT_ID>" `
    -ObjectId "<SERVICE_PRINCIPAL_OBJECT_ID>" `
    -DisplayName "Contoso Mail Reader SP"

# Step 3 — Create a management scope limited to one mailbox or a security group
New-ManagementScope -Name "MailReaderScope" `
    -RecipientRestrictionFilter { Alias -eq "notifications" }

# Step 4 — Assign the role to the service principal within that scope
New-ManagementRoleAssignment `
    -Role "Application Mail.Read" `
    -App "<CLIENT_ID>" `
    -CustomResourceScope "MailReaderScope"

Reference: Role Based Access Control for Applications in Exchange Online

Solution B: ApplicationAccessPolicy (legacy — still functional, migration required eventually)

New-ApplicationAccessPolicy is supported but is being replaced by RBAC for Applications. New deployments should prefer Solution A. Existing policies will require migration when the legacy feature is deprecated.

# Restrict an app to members of a mail-enabled security group
New-ApplicationAccessPolicy `
    -AccessRight RestrictAccess `
    -AppId "<CLIENT_ID>" `
    -PolicyScopeGroupId "app-mailbox-scope@contoso.onmicrosoft.com" `
    -Description "Restrict mail reader app to notification mailbox group only."

# Verify the policy works for a specific mailbox
Test-ApplicationAccessPolicy `
    -AppId "<CLIENT_ID>" `
    -Identity "notifications@contoso.onmicrosoft.com"

AccessRight values:

  • RestrictAccess — app may ONLY access mailboxes listed in the scope group.
  • DenyAccess — app is BLOCKED from accessing mailboxes listed in the scope group.

Reference: New-ApplicationAccessPolicy

Test your scope before going to production

# Should return "Granted"
Test-ApplicationAccessPolicy -AppId "<CLIENT_ID>" `
    -Identity "notifications@contoso.onmicrosoft.com"

# Should return "Denied" (a mailbox outside the scope group)
Test-ApplicationAccessPolicy -AppId "<CLIENT_ID>" `
    -Identity "ceo@contoso.onmicrosoft.com"

3. Secrets Management

Secrets never belong in code or committed files

The following are unacceptable:

  • Client secrets or certificates checked into version control (.env, appsettings.json, config.py, etc.)
  • Secrets in CI/CD pipeline environment variables stored in plaintext
  • Secrets logged to application output or diagnostics
  • Secrets emailed, Slacked, or shared outside a secrets manager

This repository uses placeholder values everywhere a secret would appear: <CLIENT_ID>, <TENANT_ID>, <CLIENT_SECRET>, <VAULT_URI>.

Tier 1 — Managed Identity (preferred: no secret exists at all)

When the workload runs on an Azure-hosted resource (App Service, Azure Functions, Container Apps, Azure VM, etc.), use a managed identity. Azure manages the credential lifecycle — there is no secret to rotate, leak, or revoke.

# Python example — DefaultAzureCredential picks up the managed identity automatically
from azure.identity import DefaultAzureCredential
from msgraph import GraphServiceClient

credential = DefaultAzureCredential()
client = GraphServiceClient(credential, scopes=["https://graph.microsoft.com/.default"])
# PowerShell — connect to Graph using the managed identity of the current Azure resource
Connect-MgGraph -Identity

Managed identities can be used to access Microsoft Graph directly — no client secret, no certificate, no rotation policy needed.

Reference: Managed Identities — Azure App Service

Tier 2 — Azure Key Vault (for workloads that cannot use managed identity)

If a managed identity is not available (on-premises runner, non-Azure host), store secrets in Azure Key Vault and retrieve them at runtime. The application authenticates to Key Vault using a short-lived certificate or, where possible, another managed identity.

# Retrieve a secret at runtime — never at build/deploy time
$secret = Get-AzKeyVaultSecret `
    -VaultName "kv-contoso-prod" `
    -Name "graph-client-secret" `
    -AsPlainText
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential

client = SecretClient(
    vault_url="https://kv-contoso-prod.vault.azure.net/",
    credential=DefaultAzureCredential()
)
secret = client.get_secret("graph-client-secret").value

Reference: Authenticate to Azure Key Vault

Secret rotation policy

Credential type Maximum lifetime Rotation trigger
Client secret 90 days Automated Key Vault rotation policy or pipeline alert
Certificate 1 year Certificate expiry alert in Key Vault
Managed identity token ~24 hours Automatic (Azure-managed)

Configure Key Vault expiration alerts so rotation happens before expiry, not after an incident.

.gitignore and pre-commit hygiene

Always include the following in .gitignore:

.env
*.env.*
appsettings.*.json
local.settings.json
**/*secret*
**/*credential*

Consider adding git-secrets or Microsoft Defender for DevOps to your CI pipeline to block commits containing secret patterns.


4. Conditional Access and Audit Logging

Conditional Access

Apply Conditional Access policies to the Entra Enterprise Application that represents this app. Recommended baseline:

Control Setting
Require compliant device or Hybrid Azure AD Join Enabled for interactive flows
Require MFA Enabled for all user-delegated flows
Allowed locations Restrict to expected geographic regions
Sign-in risk policy Block sign-ins with High risk (Microsoft Entra ID Protection)
Session controls Limit session lifetime for sensitive workloads

Conditional Access policies are evaluated at every token issuance, and the results — including policies applied, policies not applied, and failures — are recorded in the Entra sign-in logs.

Reference: Conditional Access and Microsoft Entra activity logs

Microsoft Entra sign-in logs

Sign-in logs capture every authentication event for the application, including:

  • Which Conditional Access policies were applied and whether they succeeded or failed
  • IP address and location of the authentication
  • Risk level assigned by Identity Protection
  • Token issuance result

Query sign-in logs for failures related to this app:

Connect-MgGraph -Scopes "AuditLog.Read.All"
Get-MgAuditLogSignIn `
    -Filter "appId eq '<CLIENT_ID>' and status/errorCode ne 0" `
    -Top 50 | Select-Object CreatedDateTime, UserPrincipalName, IpAddress, `
              ConditionalAccessStatus, Status

Microsoft Purview Unified Audit Log

The unified audit log (UAL) aggregates activity from Exchange Online, SharePoint, Teams, Entra, and Microsoft Graph API calls into a single searchable log. Retention is 90 days by default; Microsoft Purview Audit (Premium) extends this to 1 year or 10 years.

# Search audit log for Graph API calls by the application over the past 7 days
Search-UnifiedAuditLog `
    -StartDate (Get-Date).AddDays(-7) `
    -EndDate (Get-Date) `
    -Operations "Send","MailItemsAccessed","FileAccessed" `
    -ResultSize 1000 | Export-Csv "audit-export.csv" -NoTypeInformation

Key operations to monitor:

  • MailItemsAccessed — triggered when mail is read by an application (requires Audit Premium)
  • Send — email sent via Graph
  • FileAccessed / FilePreviewed — SharePoint/OneDrive file access by the app
  • ConsentToApp — new app consent granted in the tenant

Reference: Audit log activities

Alerting recommendations

Route audit log and sign-in log data to a SIEM (Microsoft Sentinel, Splunk, Elastic) or at minimum configure Microsoft Purview alert policies for:

  • App consent granted to a new application
  • Unusual volume of MailItemsAccessed events from a service principal
  • Sign-in from an unexpected location for this application

5. Data Protection When AI Touches Sensitive Data

What must never be sent to an external LLM without controls

Before routing any Microsoft 365 content through an AI model — whether Azure OpenAI, a third-party API, or an on-premises model — classify what the data contains.

Data category Examples External LLM policy
CUI (Controlled Unclassified Information) ITAR-controlled documents, DoD contract info Block — route only to FedRAMP-authorized AI services with a signed DUA/BAA
PII Names + SSN, financial account numbers, health information (PHI/HIPAA) Block or anonymize/mask before transmission; document the control
Company Confidential M&A data, unreleased product plans, attorney-client communications Evaluate on a case-by-case basis; prefer on-premises or sovereign AI
Sensitivity-labeled content Any file or email with a Purview sensitivity label of "Confidential" or above Respect label-based DLP rules; do not bypass labels in code
General business data Meeting notes, general email threads with no label May be permissible — verify your vendor's data processing agreement

Microsoft Purview Sensitivity Labels

Apply sensitivity labels to content before it enters an AI pipeline. Labels persist with the content across services and can be enforced by DLP policies.

# List available sensitivity labels in the tenant via Graph
Connect-MgGraph -Scopes "InformationProtectionPolicy.Read"
Get-MgUserInformationProtectionSensitivityLabel -UserId "me"

A DLP policy can be configured to block Graph API calls or Copilot prompts that contain content matching a specific sensitivity label or a sensitive information type (SIT) pattern (e.g., U.S. Social Security Number, credit card number).

Reference: Use sensitivity labels as conditions in DLP policies

AI prompt and response auditing

Microsoft Purview captures prompts and responses for Microsoft 365 Copilot interactions in the unified audit log, including references to files accessed and sensitivity labels present on those files. For custom AI integrations using Graph, implement equivalent logging:

  • Log the data classification of every item retrieved before forwarding to an AI model.
  • Log the AI model endpoint, request timestamp, and a hash or truncated preview of the prompt.
  • Never log full PII or CUI in application logs.

Reference: Microsoft Purview data security for Microsoft 365 Copilot

Code-level controls

BLOCKED_SENSITIVITY_LABELS = {"Highly Confidential", "CUI", "Restricted"}

def is_safe_to_send_to_llm(item_sensitivity_label: str) -> bool:
    """Return False if the item's label prohibits external AI processing."""
    return item_sensitivity_label not in BLOCKED_SENSITIVITY_LABELS

def redact_pii(text: str) -> str:
    """
    Pass content through a PII detection step (e.g., Azure AI Language PII detection)
    before forwarding to an external model.
    Placeholder — replace with actual implementation.
    """
    raise NotImplementedError("Implement PII redaction before sending to external LLM")

6. NIST SP 800-171 / CMMC Control-Family Mapping

The controls in this repository map to four key control families in NIST SP 800-171 Rev. 3 and CMMC Level 2. This section provides a generic mapping — apply it to your own System Security Plan (SSP) and Plan of Action & Milestones (POA&M).

CMMC Level 2 requires assessment against all 110 practices in NIST SP 800-171 Rev. 2. NIST SP 800-171 Rev. 3 was finalized May 14, 2024. Verify which revision applies to your specific contract.

3.1 — Access Control (AC)

NIST control Control description How this repo addresses it
3.1.1 Limit system access to authorized users and processes App registrations are scoped to minimum permissions; RBAC for Applications restricts mailbox access to explicit scope groups
3.1.2 Limit system access to the types of transactions and functions authorized users are permitted to execute Delegated vs. application permission choice documented; Mail.Read not Mail.ReadWrite.All
3.1.3 Control the flow of CUI in accordance with approved authorizations Sensitivity label checks in code gate whether content flows to external AI; DLP policies enforce at the platform layer
3.1.5 Employ the principle of least privilege Enforced at every layer: Graph permissions, RBAC scope, Key Vault access policies, Conditional Access

3.3 — Audit and Accountability (AU)

NIST control Control description How this repo addresses it
3.3.1 Create and retain system audit logs to enable monitoring, analysis, investigation, and reporting Unified Audit Log enabled; sign-in logs retained; Search-UnifiedAuditLog examples provided
3.3.2 Ensure the actions of individual users can be uniquely traced to those users Service principal identity tied to all Graph API calls; MailItemsAccessed audit events capture per-item access

3.5 — Identification and Authentication (IA)

NIST control Control description How this repo addresses it
3.5.1 Identify system users, processes, and devices Entra app registration provides a unique, auditable identity for the application; managed identity eliminates shared credentials
3.5.2 Authenticate the identities of users, processes, or devices before allowing access OAuth 2.0 client credentials flow with certificate or managed identity; MFA enforced via Conditional Access for delegated flows
3.5.3 Use multi-factor authentication for local and network access to privileged accounts Conditional Access policy requiring MFA applied to all interactive sign-ins to the application
3.5.10 Store and transmit only cryptographically-protected passwords Secrets stored in Azure Key Vault (AES-256 at rest, TLS 1.2+ in transit); plaintext secrets never in code

3.13 — System and Communications Protection (SC)

NIST control Control description How this repo addresses it
3.13.1 Monitor, control, and protect communications at external boundaries All Graph API traffic over HTTPS/TLS; Conditional Access restricts authentication to approved networks/locations
3.13.10 Establish and manage cryptographic keys for cryptography employed in organizational systems Azure Key Vault manages key lifecycle; rotation policies enforced; managed identity eliminates long-lived credential keys
3.13.15 Protect the authenticity of communications sessions OAuth 2.0 tokens are short-lived (1 hour default); refresh token rotation enforced; token audiences are scoped to the specific resource

Quick Reference Checklist

Before deploying any application in this repository to a production tenant:

  • App registration requests only the minimum Graph permissions required — no *.All unless documented and justified
  • Application permissions are scoped using RBAC for Applications or ApplicationAccessPolicy to prevent tenant-wide mailbox access
  • No secrets in code, .env files, or version control — managed identity or Key Vault only
  • Key Vault secrets have expiration dates and rotation alerts configured
  • Conditional Access policy applied to the Enterprise Application
  • Unified Audit Log is enabled and retained for the required period
  • Data classification check implemented before content is forwarded to any AI model
  • Sensitivity label and DLP policies reviewed to ensure AI processing flows are covered
  • Controls documented in SSP if the system processes CUI (NIST 800-171 / CMMC applicability)

References

There aren't any published security advisories