Setup Maester in Azure Container App Jobs
This guide will summarize the major requirements in setting up Maester in Azure Container App Jobs. You may find gaps in this guide or areas requiring clarification. Please open a discussion if you run into challenges this guide does not address.
Why Azure Container App Jobsโ
Azure Container App Jobs allow you to run custom container images and run those images on demand as needed. Although more complex than alternative methods offered, there is additional flexibility with having a base image to customize.
Pre-requisitesโ
- If this is your first time using Microsoft Azure, you must set up an Azure Subscription so you can create resources and are billed appropriately
- You must also have the Global Administrator role in your Entra tenant. This is so the necessary permissions can be consented
- An instance where you have Docker installed
- A Docker image with PowerShell Core installed
- An Azure Container Registry for hosting your image
- Optionally:
- Azure VM with a Managed Identity as your build instance (Assign Application Administrator Entra Role)
- Azure Key Vault for storing secret material
- Azure Storage Account for storing test results
Create your Entra Applicationโ
Create an Entra Applicationโ
- Open Entra admin center > Identity > Applications > App registrations
- Tip: enappreg.cmd.ms is a shortcut to the App registrations page.
- Select New registration
- Enter a name for the application (e.g.
Maester DevOps Account) - Select Register
Grant permissions to Microsoft Graphโ
- Open the application you created in the previous step
- Select API permissions > Add a permission
- Select Microsoft Graph > Application permissions
- Search for each of the permissions and check the box next to each permission:
- AuditLog.Read.All
- DeviceManagementConfiguration.Read.All
- DeviceManagementManagedDevices.Read.All
- DeviceManagementRBAC.Read.All
- DeviceManagementServiceConfig.Read.All
- Directory.Read.All
- DirectoryRecommendations.Read.All
- EntitlementManagement.Read.All
- IdentityRiskEvent.Read.All
- OnPremDirectorySynchronization.Read.All
- OrgSettings-AppsAndServices.Read.All
- OrgSettings-Forms.Read.All
- Policy.Read.All
- Policy.Read.ConditionalAccess
- PrivilegedAccess.Read.AzureAD
- Reports.Read.All
- ReportSettings.Read.All
- RoleEligibilitySchedule.Read.Directory
- RoleManagement.Read.All
- SecurityIdentitiesSensors.Read.All
- SecurityIdentitiesHealth.Read.All
- SharePointTenantSettings.Read.All
- ThreatHunting.Read.All
- UserAuthenticationMethod.Read.All
- Optionally, search for each of the permissions if you want to allow privileged permissions:
- ReportSettings.ReadWrite.All
- Required to disable report obfuscation
- ReportSettings.ReadWrite.All
- Select Add permissions
- Select Grant admin consent for [your organization]
- Select Yes to confirm
(Optional) Grant permissions to Exchange Online
(Optional) Grant permissions to Exchange Onlineโ
The Exchange Online Role Based Access Control (RBAC) implementation utilizes service specific roles that apply to an application and the below configuration allows the authorization chain to the App Registration you created in the previous steps.
The Exchange Online permissions are necessary to support tests that validate Exchange Online configurations, such as the CISA tests.
- Open the application you created in the previous step
- Select API permissions > Add a permission
- Select APIs that my organization uses > search for Office 365 Exchange Online > Application permissions
- Search for
Exchange.ManageAsApp - Select Add permissions
- Select Grant admin consent for [your organization]
- Select Yes to confirm
- Connect to the Exchange Online Management tools and use the following to set the appropriate permissions:
New-ServicePrincipal -AppId <Application ID> -ObjectId <Object ID> -DisplayName <Name>
New-ManagementRoleAssignment -Role "View-Only Configuration" -App <DisplayName from previous command>
(Optional) Grant permissions to Teams
(Optional) Grant permissions to Teamsโ
The Teams Role Based Access Control (RBAC) implementation utilizes service specific roles that apply to an application and the below configuration allows the authorization chain to the App Registration you created in the previous steps.
The Teams permissions are necessary to support tests that validate Teams configurations.
- Open Roles and administrators
- Search and select Teams Reader
- Select Add assigment
- Select No member selected
- Search for the name of previously created application
- Select previously created application and select Select to confirm
- Select Next to confirm
- Ensure that Active and Permanently assigned are ticked
- Enter Justification
- Select Assign to confirm
(Optional) Grant permissions to Azure
(Optional) Grant permissions to Azureโ
The Azure Role Based Access Control (RBAC) implementation utilizes Uniform Resource Names (URN) with a "/" separator for heirarchical scoping. There exists resources within the root (e.g., "/") scope that Microsoft retains strict control over by limiting supported interactions. As a Global Administrator you can elevate access to become authorized for these limited interactions.
The Azure RBAC permissions are necessary to support tests that validate Azure configurations, such as the CISA tests.
The following PowerShell script will enable you, with a Global Administrator role assignment, to:
- Identify the Service Principal Object ID that will be authorized as a Reader (Enterprise app Object ID)
- Install the necessary Az module and prompt for connection
- Elevate your account access to the root scope
- Create a role assignment for Reader access over the Root Scope
- Create a role assignment for Reader access over the Entra ID (i.e., aadiam provider)
- Identify the role assignment authorizing your account access to the root scope
- Delete the root scope role assignment for your account
$servicePrincipal = "<Object ID of the Entra App>"
$subscription = "<Subscription ID>"
Install-Module Az.Accounts -Force
Install-Module Az.Resources -Force
Connect-AzAccount
#Elevate to root scope access
$elevateAccess = Invoke-AzRestMethod -Path "/providers/Microsoft.Authorization/elevateAccess?api-version=2015-07-01" -Method POST
#Assign permissions to Enterprise App
New-AzRoleAssignment -ObjectId $servicePrincipal -Scope "/" -RoleDefinitionName "Reader" -ObjectType "ServicePrincipal"
New-AzRoleAssignment -ObjectId $servicePrincipal -Scope "/providers/Microsoft.aadiam" -RoleDefinitionName "Reader" -ObjectType "ServicePrincipal"
#Remove root scope access
$assignment = Get-AzRoleAssignment -RoleDefinitionId 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9|?{$_.Scope -eq "/" -and $_.SignInName -eq (Get-AzContext).Account.Id}
$deleteAssignment = Invoke-AzRestMethod -Path "$($assignment.RoleAssignmentId)?api-version=2018-07-01" -Method DELETE
(Optional) Grant permissions to SharePoint Online
(Optional) Grant permissions to SharePoint Onlineโ
SharePoint Online tests require the PnP.PowerShell module and an Entra ID app registration configured for interactive login with SharePoint delegated permissions.
The SharePoint Online permissions are necessary to support tests that validate SharePoint Online configurations, such as the CISA SharePoint baseline controls.
Install PnP.PowerShellโ
Install-Module PnP.PowerShell -Scope CurrentUser
Option A โ Automatically create a new PnP app registrationโ
PnP provides a built-in cmdlet to create a dedicated app registration for interactive login:
Register-PnPEntraIDAppForInteractiveLogin -ApplicationName "Maester PnP" -Tenant [yourtenant].onmicrosoft.com -SharePointDelegatePermissions "AllSites.FullControl"
This will:
- Create an Entra ID app registration with the required delegated permissions
- Configure
http://localhostas the redirect URI automatically - Prompt you to authenticate and provide consent
- Output the Client ID you will need for
Connect-Maester
Note: Maester's SharePoint tests are read-only.
AllSites.FullControl(delegated) is sufficient.Important: After registering the app, open a new PowerShell session before running
Connect-Maester, as the registration process loads PnP assemblies that can conflict with Microsoft Graph.
Option B โ Reuse the existing Maester app registrationโ
If you prefer not to create a separate app, you can update the Maester app registration you created earlier to also support PnP PowerShell interactive login.
1. Add the localhost redirect URI
- Open your Maester app registration in the Entra admin center
- Select Authentication > Add a platform > Mobile and desktop applications
- In the Custom redirect URIs field, enter
http://localhost(note:http, nothttps) - Select Configure
2. Add SharePoint delegated permissions
- Select API permissions > Add a permission
- Select SharePoint > Delegated permissions
- Search for and check
AllSites.FullControl - Select Add permissions
- Select Grant admin consent for [your organization] and confirm
3. Connect using the existing Client ID
You can retrieve the Client ID of your Maester app from Graph without looking it up manually. First connect to Graph, then query for the app by its display name:
# Connect to Graph first
Connect-Maester -Service Graph
# Retrieve the Client ID of the Maester app registration
$clientId = (Get-MgApplication -Filter "displayName eq 'Maester DevOps Account'").AppId
Then connect to SharePoint Online in a new session using the retrieved Client ID. The SharePoint admin URL is auto-discovered from your tenant โ no need to specify it manually:
# Open a new PowerShell session, then:
Connect-Maester -Service Graph,SharePointOnline -SharePointClientId $clientId
Important: After registering the app, open a new PowerShell session before running
Connect-Maester, as PnP assemblies loaded during the session can conflict with Microsoft Graph.
Option C โ App-only access (automation / non-interactive)โ
For unattended runs (CI pipelines, scheduled tasks) where interactive login is not possible, use application permissions with certificate-based authentication.
1. Add SharePoint application permission
- Open your app registration in the Entra admin center
- Select API permissions > Add a permission
- Select SharePoint > Application permissions
- Search for and check
Sites.FullControl.All - Select Add permissions
- Select Grant admin consent for [your organization] and confirm
2. Upload a certificate
- Select Certificates & secrets > Certificates > Upload certificate
- Upload the public key (
.cer) of your certificate
3. Connect using Connect-Maester
Pass the certificate thumbprint directly to Connect-Maester using -SharePointCertificateThumbprint. -TenantId is required for thumbprint-based auth:
$params = @{
Service = @('Graph', 'SharePointOnline')
SharePointClientId = "<App Client ID>"
SharePointCertificateThumbprint = "<Certificate Thumbprint>"
TenantId = "<Tenant ID or domain>"
}
Connect-Maester @params
The certificate must be present in the current user's Windows certificate store. The SharePoint admin URL is auto-discovered โ supply -SharePointAdminUrl to override it if needed.
Use the Client ID from Option A or Option B when connecting:
Connect-Maester -Service Graph,SharePointOnline -SharePointClientId "<Client ID>"
The SharePoint admin URL is auto-discovered from your tenant's initial domain. If auto-discovery does not work (e.g. government or custom-domain tenants), supply it explicitly:
Connect-Maester -Service Graph,SharePointOnline -SharePointClientId "<Client ID>" -SharePointAdminUrl "https://contoso-admin.sharepoint.com"
For device code flow (e.g. non-interactive sessions):
Connect-Maester -Service Graph,SharePointOnline -SharePointClientId "<Client ID>" -UseDeviceCode
(Optional) Grant Dataverse permissions for Copilot Studio tests
(Optional) Grant Dataverse permissions for Copilot Studioโ
Dataverse access is required for the Copilot Studio security tests (MT.1113โMT.1122) that evaluate Copilot Studio agent configurations.
Create an Application User in Power Platformโ
- Go to the Power Platform Admin Center โ select your environment โ Settings โ Users + permissions โ Application users
- Click New app user โ Add an app โ select the app registration created above
- Select the correct Business unit
- Assign a security role with read access:
- Basic User for simplicity, or
- A custom role (e.g.
Maester Security Reader) with Organization-level Read on: Agent (bot), Agent component (botcomponent), User (systemuser), and Connection Reference (connectionreference)
- Click Create
Configure Maesterโ
Add the environment URL to maester-config.json:
{
"GlobalSettings": {
"DataverseEnvironmentUrl": "https://org12345.crm.dynamics.com"
}
}
Configure Certificate Based Authentication for your Service Principalโ
The following PowerShell script will enable you to:
- Identify the Service Principal Application (Client) ID and Display Name and an existing Azure Key Vault Name
- Install the necessary modules and prompt for authentication to Azure and Graph
- If you are using a system with a managed identity for your build environment you can use the
-Identityswitch for the connection commands. - Define a certificate policy and request Key Vault to create the certifcate
- โ ๏ธ This policy creates a certificate that will expire after 12 months, ensure you update it appropriately
- Wait until the certificate becomes available in the Key Vault
- Retrieve the public key from the Key Vault
- Set the public key as an authentication method for the Entra Application Registration
Alternatively, if you prefer not to use a Key Vault, Microsoft provides guidance to perform similar steps.
$applicationId = "<Application (Client) ID>"
$applicationDisplayName = "<Application Display Name"
$keyVaultName = "<Key Vault Name>"
Install-Module Az.Accounts -Force
Install-Module Az.KeyVault -Force
Connect-AzAccount
Install-Module Microsoft.Graph.Authentication -Force
Connect-MgGraph
$Policy = New-AzKeyVaultCertificatePolicy -SecretContentType "application/x-pkcs12" -SubjectName "CN=$applicationDisplayName" -IssuerName "Self" -ValidityInMonths 12 -ReuseKeyOnRenewal
Add-AzKeyVaultCertificate -VaultName $keyVaultName -Name $applicationDisplayName -CertificatePolicy $Policy
$status = $false
while($status){
if((Get-AzKeyVaultCertificateOperation -VaultName $keyVaultName -Name $applicationDisplayName).Status -eq "completed"){
$status = $true
}else{
"Cert not issued, waiting";Start-Sleep -Seconds 5
}
}
$kvCert = Get-AzKeyVaultCertificate -VaultName $keyVaultName -Name $applicationDisplayName
$body = @{
keyCredentials = @(
@{
endDateTime = $kvCert.Certificate.notAfter
startDateTime = $kvCert.Certificate.notBefore
type = "AsymmetricX509Cert"
usage = "Verify"
key = $([Convert]::ToBase64String($kvCert.Certificate.RawData))
displayName = $kvCert.Certificate.subject
}
)
} | ConvertTo-Json
Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/v1.0/applications(appId='$applicationId')" -Body $body
Create your Docker imageโ
Using Docker you can define process steps and save those steps as layers to an image. When you build an image it is possible for secret material to exist in the image's layers. To avoid this there are two components you will use below. The first is a PowerShell script, main.ps1, that you will instruct the Docker image to execute each time the image is run. The second is a simple Dockerfile that provides all the prerequisities for the PowerShell script.
The following PowerShell script will enable you to:
- Define the key aspects of your environment
- Update any Maester tests each time the container is run
- Connect to the environment as the container's managed identity
- Obtain the private key of your Serivce Principal to authenticate against Entra with
- Connect to the environment as the Service Principal
- Run Maester
- Sync the results with an Azure Storage Account
- Alternatively you can utilize a Git repo
- The below example uses Storage Account connection strings that the system assigned managed identity retrieves, alternatively you can use a user assigned managed identity to avoid connection strings
- Compare the test results for the last two tests
### main.ps1
$applicationId = "<Application (Client) ID>"
$tenantId = "<Tenant ID you want to run Maester against>"
$applicationDisplayName = "<Application Display Name"
$keyVaultName = "<Key Vault Name>"
$storageAccountName = "<Storage Account Name>"
$storageAccountResourceGroupName = "<Name of Resource Group Storage Account exists>"
Update-MaesterTests
#Connect to serivce provider tenant
Connect-MgGraph -Identity -NoWelcome
Connect-AzAccount -Identity
#Get SPN credential
$b64 = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $applicationDisplayName -AsPlainText
$bytes = [Convert]::FromBase64String($b64)
Set-Content -Path /cert.pfx -value $bytes -AsByteStream
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)
#Connect to target tenant
Connect-MgGraph -AppId $applicationId -Certificate $cert -TenantId $tenantId -NoWelcome
$domains = Invoke-MgGraphRequest -Uri https://graph.microsoft.com/v1.0/domains
$moera = ($domains.value|?{$_.isInitial}).id
#Cmdlet load kills docker image at 1GB memory node
Connect-ExchangeOnline -Certificate $cert -AppID $applicationId -Organization $moera -ShowBanner:$false
Connect-IPPSSession -Certificate $cert -AppID $applicationId -Organization $moera -ShowBanner:$false
Connect-AzAccount -ServicePrincipal -ApplicationId $applicationId -TenantId $tenantId -CertificatePath /cert.pfx
#Run Maester
Invoke-Maester -SkipGraphConnect -NonInteractive
#Reconnect to service provider tenant
Connect-AzAccount -Identity
#Sync results with storage account
$stAccount = Get-AzStorageAccount -Name $storageAccountName -ResourceGroupName $storageAccountResourceGroupName
$stContainer = Get-AzStorageContainer -Context $stAccount.Context -Name $tenantId
if(-not $stContainer){$stContainer = New-AzStorageContainer -Context $stAccount.Context -Name $tenantId}
gci ./test-results/|%{Set-AzStorageBlobContent -Container $stContainer.Name -Context $stAccount.Context -File $_ -Blob $_.Name -Force|Out-Null}
$jsonResults = Get-AzStorageBlob -Container $stContainer.Name -Context $stAccount.Context -Blob "TestResults*.json"
#$jsonResults.Name.Substring(12,17)|%{[DateTime]::ParseExact($_,"yyyy-MM-dd-HHmmss",$null)}
$compare = ($jsonResults|Sort-Object $_.LastModified.DateTime -Descending)[-2]
Get-AzStorageBlobContent -Container $stContainer.Name -Context $stAccount.Context -Blob $compare.Name -Destination /maester/test-results/|Out-Null
$comparison = Compare-MtTestResult -BaseDir /maester/test-results
$comparisonFile = New-Item -Path /maester/ -Name "compare-$($compare.Name)" -Value $($comparison|ConvertTo-Json)
Set-AzStorageBlobContent -Container $stContainer.Name -Context $stAccount.Context -File $comparisonFile -Blob $comparisonFile.Name -Force|Out-Null
The following Dockerfile will enable you to prepare the image to successfully run the main.ps1 file.
FROM mcr.microsoft.com/powershell
SHELL ["pwsh","-Command"]
COPY main.ps1 /
RUN New-Item /maester -ItemType Directory
WORKDIR "/maester"
RUN Install-Module Az.Accounts -Force
RUN Install-Module Az.KeyVault -Force
RUN Install-Module Az.Storage -Force
RUN Install-Module Microsoft.Graph.Authentication -Force
RUN Install-Module ExchangeOnlineManagement -Force
RUN Install-Module Maester -Force
CMD & /main.ps1
Push your image to ACRโ
With your Azure Container Registry setup and authorizing your build instance managed identity for pushing, you can use the following process to properly tag and push your image.
The
docker buildexample assumes you place both themain.ps1andDockerfilefiles in the current working directory.
sudo docker build -t maesterjob .
sudo docker tag maesterjob <yourRegistry>.azurecr.io/maesterjob
sudo pwsh -command "Connect-AzAccount -Identity"
sudo pwsh -command "Connect-AzContainerRegistry -Name <yourRegistry>"
sudo docker push <yourRegistry>.azurecr.io/maesterjob
Create your Azure Container App Jobโ
Begin by creating a new Azure Container App Job. You can use a simple cron job, an event trigger, or you can always manually invoke the job as well. Your Azure Container Registry will need to have an admin access key enabled for the Azure Portal process to succeed.
For the CPU and memory, you may find that 1 GB of memory is not enough and 1.5 GB or 2 GB will offer more consistent success.