Automated Microsoft 365 tenant configuration management using PowerShell Desired State Configuration (DSC) and GitHub Actions CI/CD pipeline. This solution enforces security baselines, maintains compliance with regulatory frameworks, and provides continuous drift detection.
This project provides infrastructure-as-code management for Microsoft 365 tenants using Microsoft365DSC. It enables you to define your desired M365 configuration declaratively and automatically deploy changes through a CI/CD pipeline while maintaining alignment with security compliance frameworks.
- Declarative Configuration: Define M365 settings in PowerShell data files
- Automated Deployment: GitHub Actions workflow builds and deploys configurations on push to main
- Multi-Workload Support: Manages Exchange, Teams, SharePoint, Azure AD, Intune, and more
- Compliance Monitoring: Built-in compliance checking with email/Teams notifications
- Drift Detection: Continuous monitoring to detect unauthorized configuration changes
- Security Baseline Enforcement: Automated enforcement of organizational security policies
- Service Principal Automation: Terraform modules for creating properly-scoped service principals
┌─────────────────────────────────────────────────────────────────┐
│ GitHub Repository │
├─────────────────────────────────────────────────────────────────┤
│ Datafiles/ Configuration data (.psd1) │
│ M365Config/ Composite DSC resources │
│ M365Configuration.ps1 Main DSC configuration │
│ build.ps1 MOF compilation script │
│ deploy.ps1 Deployment script │
│ checkdsccompliancy.ps1 Drift detection script │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GitHub Actions Workflow │
│ 1. Checkout repository │
│ 2. Build MOF files (build.ps1) │
│ 3. Deploy to M365 tenant (deploy.ps1) │
│ 4. Scheduled drift detection (checkdsccompliancy.ps1) │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Microsoft 365 Tenant │
│ Exchange │ Teams │ SharePoint │ Azure AD │ Intune │ OneDrive │
└─────────────────────────────────────────────────────────────────┘
The GitHub Actions workflow (.github/workflows/build.yaml) implements a complete CI/CD pipeline for M365 configuration management:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Commit to │────▶│ Build │────▶│ Deploy │────▶│ M365 │
│ main │ │ MOF Files │ │ Config │ │ Tenant │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Validation │ │ Audit │
│ & Testing │ │ Logging │
└──────────────┘ └──────────────┘
1. Trigger (on push to main)
on:
push:
branches:
- main2. Build Stage (build.ps1)
- Validates Microsoft365DSC module version
- Installs required dependencies
- Compiles DSC configurations into MOF files
- Validates configuration syntax and references
3. Deploy Stage (deploy.ps1)
- Authenticates using service principal credentials
- Applies MOF configuration to M365 tenant
- Reports success/failure status
Configure the following secrets in your GitHub repository (Settings → Secrets and variables → Actions):
| Secret | Description | Usage |
|---|---|---|
CLIENT_ID |
Service principal application (client) ID | Authentication to M365 |
CLIENT_SECRET |
Service principal client secret | Authentication to M365 |
name: Build and Deploy MOF
on:
push:
branches:
- main
jobs:
job1:
runs-on: windows-latest
name: Build MOF
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get Creds, Build, Deploy
shell: powershell
run: |
$username = "${{secrets.CLIENT_ID}}"
$password = ConvertTo-SecureString "${{secrets.CLIENT_SECRET}}" -AsPlainText -Force
$psCreds = New-Object System.Management.Automation.PSCredential -ArgumentList ($username, $password)
.\build.ps1 -AdminCreds $psCreds
.\deploy.ps1 -Environment ProductionTo add additional stages (e.g., scheduled compliance checks):
# Add to .github/workflows/build.yaml or create a new workflow
name: Compliance Check
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
jobs:
compliance:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Run Compliance Check
run: .\checkdsccompliancy.ps1This solution enforces security configurations through a desired state model:
- Define: Security requirements are codified in
.psd1data files - Compile: DSC compiles these into machine-readable MOF files
- Apply: Configuration is pushed to M365, overwriting non-compliant settings
- Monitor: Drift detection identifies unauthorized changes
- Remediate: Pipeline automatically re-applies compliant configuration
| Configuration | Security Benefit |
|---|---|
ActivationReqMFA = $true |
Requires MFA for PIM role activation |
AutoAdmittedUsers = "EveryoneInCompanyExcludingGuests" |
Restricts Teams meeting auto-admit |
AllowAnonymousUsersToStartMeeting = $false |
Prevents anonymous meeting initiation |
DisableAnonymousJoin = $false |
Controls anonymous access to meetings |
AllowGuestUser = $true (controlled) |
Managed guest access with restrictions |
| Configuration | Security Benefit |
|---|---|
AllowCloudRecording = $false |
Prevents unauthorized recording |
EnableSafeDocs = $true |
Enables Safe Documents for ATP |
EnableATPForSPOTeamsODB = $true |
ATP protection for SharePoint/OneDrive |
| DLP Policies (U.S. Financial Data) | Prevents sensitive data exfiltration |
| Sensitivity Labels | Information classification enforcement |
| Configuration | Security Benefit |
|---|---|
DKIM Signing (Enabled = $true) |
Email authentication |
EwsApplicationAccessPolicy = "EnforceBlockList" |
Controls EWS application access |
AuditDisabled = $False |
Ensures audit logging is active |
| Configuration | Security Benefit |
|---|---|
LegacyAuthProtocolsEnabled = $false |
Blocks legacy authentication |
DisableEnvironmentCreationByNonAdminUsers = $True |
Restricts Power Platform environment creation |
AllowBox/DropBox/GoogleDrive = $False |
Restricts third-party cloud storage |
SecurityBlockJailbrokenDevices = $True |
Blocks compromised iOS devices |
| Configuration | Security Benefit |
|---|---|
PermanentActiveAssignmentisExpirationRequired = $true |
Time-limited privileged access |
ExpireActiveAssignment = "P15D" |
15-day maximum for active assignments |
ActivationReqJustification = $true |
Requires justification for elevation |
| Alert notifications to security team | Real-time privileged access monitoring |
EXOOrganizationConfig 'EXOOrganizationConfig'
{
AuditDisabled = $False # Ensure auditing enabled
AutoExpandingArchive = $False # Control archive expansion
DefaultGroupAccessType = "Private" # Private groups by default
EwsApplicationAccessPolicy = "EnforceBlockList" # Control EWS access
OAuth2ClientProfileEnabled = $True # Modern auth enabled
}AADRoleSetting 'Global Administrator'
{
ActivationReqMFA = $true # Require MFA
ActivationReqJustification = $true # Require justification
PermanentActiveAssignmentisExpirationRequired = $true # No permanent access
ExpireActiveAssignment = "P15D" # 15-day max
ActiveAlertNotificationAdditionalRecipient = "security_notifications@domain.com"
}This solution supports compliance with multiple regulatory frameworks. Below are mappings to NIST 800-171 control families.
| Control | Description | Implementation |
|---|---|---|
| AC-2 | Account Management | Azure AD role settings with time-limited assignments, PIM policies requiring justification and approval |
| AC-3 | Access Enforcement | Teams policies restricting meeting access, SharePoint conditional access policies |
| AC-5 | Separation of Duties | Separate service principals per workload with least-privilege permissions |
| AC-6 | Least Privilege | PIM role settings with ExpireActiveAssignment, ExpireEligibleAssignment |
| AC-7 | Unsuccessful Logon Attempts | Azure AD authentication policies (configured via Conditional Access) |
| AC-11 | Session Lock | ActivityBasedAuthenticationTimeoutEnabled = $True with 6-hour timeout |
| AC-17 | Remote Access | Teams guest policies, external sharing restrictions in SharePoint/OneDrive |
| AC-19 | Access Control for Mobile | Intune compliance policies (SecurityBlockJailbrokenDevices = $True) |
| AC-20 | Use of External Systems | AllowBox/DropBox/GoogleDrive = $False in Teams client configuration |
| Control | Description | Implementation |
|---|---|---|
| IA-2 | Identification and Authentication | ActivationReqMFA = $true for privileged roles, Azure AD authentication policies |
| IA-2(1) | MFA for Network Access | MFA required for PIM activation |
| IA-2(2) | MFA for Local Access | Intune device compliance requiring passcode |
| IA-4 | Identifier Management | Service principal lifecycle managed via Terraform with rotation |
| IA-5 | Authenticator Management | service_principal_password_rotation_in_years = 1 for credential rotation |
| IA-8 | Identification of Non-Org Users | Guest user policies in Teams, SharePoint external sharing controls |
| Control | Description | Implementation |
|---|---|---|
| SC-7 | Boundary Protection | Exchange inbound/outbound connectors, Teams federation settings |
| SC-8 | Transmission Confidentiality | RequireTls = $true on Exchange connectors, DKIM signing enabled |
| SC-13 | Cryptographic Protection | DKIM with 1024-bit keys, TLS enforcement |
| SC-23 | Session Authenticity | OAuth2 enabled, legacy auth disabled (LegacyAuthProtocolsEnabled = $false) |
| SC-28 | Protection of Information at Rest | Sensitivity labels with encryption capabilities |
| Control | Description | Implementation |
|---|---|---|
| SI-2 | Flaw Remediation | Automated configuration enforcement via CI/CD pipeline |
| SI-3 | Malicious Code Protection | EnableATPForSPOTeamsODB = $True, Safe Documents enabled |
| SI-4 | System Monitoring | AuditDisabled = $False, PIM alert notifications |
| SI-7 | Software and Information Integrity | DSC drift detection, configuration-as-code with version control |
| CIS Control | Implementation |
|---|---|
| 1.1.1 - Enable MFA | PIM ActivationReqMFA = $true |
| 2.1 - Block legacy auth | LegacyAuthProtocolsEnabled = $false |
| 3.1 - Enable audit logging | AuditDisabled = $False |
| 5.2 - Enable ATP | EnableATPForSPOTeamsODB = $True |
The NIST 800-171 mappings above provide substantial coverage for FedRAMP Moderate baseline requirements, particularly in AC, IA, SC, and SI families.
The checkdsccompliancy.ps1 script generates compliance reports suitable for audit evidence:
# Report includes:
# - Timestamp of compliance check
# - Environment tested
# - Resources in desired state
# - Resources NOT in desired state (with details)
# - Overall compliance statusDrift occurs when the actual M365 configuration deviates from the desired state defined in code. This can happen through:
- Manual changes in admin portals
- Changes via PowerShell/API outside the pipeline
- Microsoft service updates
- Malicious configuration changes
┌─────────────────────────────────────────────────────────────────┐
│ Drift Detection Process │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Desired │ │ Actual │ │
│ │ State │◄───── Compare ────▶│ State │ │
│ │ (MOF File) │ │ (M365 API) │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
│ └─────────────┬─────────────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Drift │ │
│ │ Report │ │
│ └──────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Email │ │ Teams │ │ Pipeline │ │
│ │ Alert │ │ Alert │ │ Trigger │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
.\checkdsccompliancy.ps1Add a scheduled workflow to run drift detection regularly:
# .github/workflows/drift-detection.yaml
name: Drift Detection
on:
schedule:
- cron: '0 */4 * * *' # Every 4 hours
workflow_dispatch: # Allow manual trigger
jobs:
check-drift:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Install Dependencies
shell: powershell
run: |
$modules = Import-PowerShellDataFile -Path 'DscResources.psd1'
Install-Module -Name 'Microsoft365Dsc' -RequiredVersion $modules.Microsoft365Dsc -Force
Update-M365DSCDependencies
- name: Run Compliance Check
shell: powershell
run: .\checkdsccompliancy.ps1
env:
MAIL_APP_ID: ${{ secrets.MAIL_APP_ID }}
MAIL_APP_SECRET: ${{ secrets.MAIL_APP_SECRET }}
TEAMS_WEBHOOK: ${{ secrets.TEAMS_WEBHOOK }}The compliance check produces detailed reports:
************************************************************
* Testing compliancy on all environments *
************************************************************
Processing all MOF files in 'C:\...\Output'
- Found 1 files
* Processing Production
- InDesiredState: False
- ErrorCount: 2
- ErroredResources:
[TeamsMeetingPolicy]MeetingPolicy_Global
[EXOOrganizationConfig]EXOOrganizationConfig
Configure alerts in checkdsccompliancy.ps1:
# Email Notifications
$useMail = $true
$mailAppId = '<APP_ID>'
$mailAppSecret = '<SECRET>'
$mailTenantId = '<TENANT_ID>'
$mailFrom = 'dsc-alerts@yourdomain.com'
$mailTo = 'security-team@yourdomain.com'
# Teams Notifications
$useTeams = $true
$teamsWebhook = 'https://outlook.office.com/webhook/...'When drift is detected, you have two remediation options:
Option 1: Manual Review (Recommended for Production)
- Review drift report
- Investigate cause of drift
- Update code if change is legitimate
- Re-run pipeline to enforce desired state
Option 2: Automatic Remediation Add auto-remediation to the drift detection workflow:
- name: Auto-Remediate Drift
if: failure() # Only if drift detected
shell: powershell
run: |
$creds = # ... setup credentials
.\deploy.ps1 -Environment Production
⚠️ Warning: Automatic remediation should be used carefully. Always investigate drift causes before enabling auto-remediation.
- Run frequently: Schedule checks every 4-6 hours minimum
- Alert immediately: Configure real-time notifications for security team
- Investigate all drift: Treat unexpected drift as a potential security incident
- Document exceptions: If drift is intentional, update the desired state in code
- Correlate with logs: Cross-reference drift with Azure AD audit logs
- Microsoft 365 tenant with appropriate licenses
- GitHub repository with Actions enabled
- Service principal(s) with required permissions (see Service Principal Setup)
- PowerShell 5.1+ (workflow runs on
windows-latest)
.
├── .github/
│ └── workflows/
│ └── build.yaml # CI/CD pipeline definition
├── Datafiles/
│ └── Production.psd1 # Environment configuration data
├── M365Config/
│ └── 1.0.0/
│ ├── M365Config.psd1 # Module manifest
│ └── DscResources/
│ ├── AzureAD/ # Azure AD configuration
│ ├── Exchange/ # Exchange Online configuration
│ ├── Intune/ # Intune configuration
│ ├── Office365/ # Office 365 settings
│ ├── OneDrive/ # OneDrive configuration
│ ├── PowerPlatform/ # Power Platform settings
│ ├── SecurityCompliance/ # Security & Compliance settings
│ ├── SharePoint/ # SharePoint Online configuration
│ └── Teams/ # Microsoft Teams configuration
├── dscspn/ # Terraform for service principals
│ ├── spn-module/ # Reusable SPN module
│ └── modules.*.spn.tf # Workload-specific SPN definitions
├── M365Configuration.ps1 # Main DSC configuration
├── DscResources.psd1 # Microsoft365DSC version pinning
├── build.ps1 # MOF compilation script
├── deploy.ps1 # Deployment script
└── checkdsccompliancy.ps1 # Drift detection script
Environment-specific configurations are stored in Datafiles/*.psd1. Each file defines settings for a specific environment (e.g., Production, Development).
# Datafiles/Production.psd1
@{
AllNodes = @(
@{
NodeName = 'localhost'
PsDscAllowPlainTextPassword = $true
PsDscAllowDomainUser = $true
}
)
NonNodeData = @{
Environment = @{
Name = 'Production'
ShortName = 'PRD'
OrganizationName = "yourdomain.com"
AlertName = "security_notifications@yourdomain.com"
}
Accounts = @(
@{ Workload = 'Exchange' }
@{ Workload = 'Teams' }
# ... additional workloads
)
Exchange = @{ ... }
Teams = @{ ... }
AzureAD = @{ ... }
}
}| Workload | Description |
|---|---|
| Exchange | Organization config, accepted domains, DKIM, connectors, mail tips |
| Teams | Meeting policies, guest settings, client configuration, emergency calling |
| SharePoint | Tenant settings, CDN configuration |
| AzureAD | PIM role settings, notifications |
| Intune | Device compliance policies |
| Office365 | Org customization, audit log settings |
| OneDrive | Storage quotas, sharing settings |
| PowerPlatform | Environment creation policies, tenant settings |
| SecurityCompliance | DLP policies, sensitivity labels |
The Microsoft365DSC module version is pinned in DscResources.psd1:
@{
Microsoft365DSC = '1.22.1019.1'
}Update this version carefully and test thoroughly before deploying to production.
The dscspn/ directory contains Terraform configurations to create properly-scoped service principals for each M365 workload.
cd dscspn
# Initialize Terraform
terraform init
# Review the plan
terraform plan
# Apply the configuration
terraform applyThis creates separate service principals with least-privilege permissions:
| Service Principal | Purpose | Key Permissions |
|---|---|---|
dsc_aad_spn |
Azure AD management | Directory.ReadWrite.All, RoleManagement.ReadWrite.Directory |
dsc_exchange_spn |
Exchange Online | Exchange Administrator role |
dsc_teams_spn |
Teams management | Teams Administrator role |
dsc_sharepoint_spn |
SharePoint management | SharePoint Administrator role |
dsc_intune_spn |
Intune management | Intune Administrator role |
dsc_o365_spn |
Office 365 | Groups Administrator role |
dsc_OD_spn |
OneDrive | Sites.FullControl.All |
dsc_platform_spn |
Power Platform | Contributor role |
dsc_security_compliance_spn |
Security & Compliance | Security Administrator role |
# Run as Administrator
$creds = Get-Credential
.\build.ps1 -AdminCreds $creds.\deploy.ps1 -Environment Production# Configure notification settings in the script first
.\checkdsccompliancy.ps1Add configuration data to the appropriate section in Datafiles/Production.psd1:
Teams = @{
MeetingPolicies = @(
@{
Identity = "CustomPolicy"
AllowCloudRecording = $true
# ... additional settings
}
)
}If adding a new resource type, update the corresponding schema file in M365Config/1.0.0/DscResources/:
# M365Config/1.0.0/DscResources/Teams/Teams.schema.psm1
foreach ($MeetingPolicy in $ConfigurationData.NonNodeData.Teams.MeetingPolicies)
{
TeamsMeetingPolicy "MeetingPolicy_$($MeetingPolicy.Identity)"
{
Identity = $MeetingPolicy.Identity
# ... map properties
}
}git add .
git commit -m "Add new Teams meeting policy"
git push origin mainThe GitHub Actions workflow will automatically build and deploy the changes.
MOF Compilation Fails
- Verify Microsoft365DSC module version matches
DscResources.psd1 - Check PowerShell execution policy:
Set-ExecutionPolicy Unrestricted -Force - Ensure all required modules are installed
Deployment Authentication Errors
- Verify service principal credentials in GitHub secrets
- Check service principal has required permissions
- Ensure admin consent is granted for Graph API permissions
Compliance Check Failures
- Review the detailed error output in the compliance report
- Check if the resource was manually modified in the tenant
- Verify the configuration data matches the expected schema
Drift Detection Shows False Positives
- Ensure Microsoft365DSC version matches between build and check
- Some M365 settings have eventual consistency; re-run after a few minutes
- Check for settings that Microsoft auto-modifies
- GitHub Actions logs: Repository → Actions → Select workflow run
- Local logs: Scripts output timestamped messages to console
- Protect Secrets: Never commit credentials to the repository
- Least Privilege: Use workload-specific service principals
- Audit Access: Monitor service principal sign-ins in Azure AD
- Review Changes: Require pull request reviews for configuration changes
- Test First: Use a development tenant before applying to production
- Microsoft365DSC Documentation
- Microsoft365DSC GitHub
- DSC Resource Reference
- NIST 800-171 Publication
- CIS Microsoft 365 Benchmarks
- Azure AD Service Principal Permissions
This project is provided as-is. See individual file headers for any specific licensing information.