For obvious reasons, it is desirable for an organisation’s cloud estate to be kept secure. Azure Policy is a tool that offers a great level of governance in this regard. While built-in security Policies/Initiatives do exist, it’s often desirable or even necessary to use an approach more tailored to a specific set of requirements.
The examples in this article will make use of available PowerShell modules to write custom Machine Configuration packages (DSC v2) with the goal of auditing or enforcing a simple industry-standard security rule.
Note: this process is not currently possible using MacOS.
1 – Setting up an Authoring Environment
1.1 Install PowerShell
For Windows the process requires an installation of PowerShell 7.1.3, with the ability to run it as Administrator.
1.2 Install other required modules
After opening PowerShell, run the following to install Guest Configuration module and if not already present, install Az:
Install-Module -Name GuestConfiguration -scope AllUsers
Install-Module -Name Az -scope AllUsers
Installation of the following modules will provide resources for writing configurations:
Install-Module -Name PSDscResources -scope AllUsers
Install-Module -Name SecurityPolicyDsc -scope AllUsers
Install-Module -Name AuditPolicyDsc -scope AllUsers
2 – Choose a ‘Rule’ and Write a Configuration to Enforce it
So far, little has been mentioned about which rules can or will be implemented. Thankfully, the modules mentioned above provide the capability to enforce a large range of security rules with hardly any coding required. This example will aim to implement:
“Ensure ‘Turn on convenience pin sign-in’ is set to ‘Disabled’.”
This setting can be accessed by the ‘Registry’ resource of the ‘PSDscResources’ module. Unfortunately for a given rule there is no quick way to determine which module and resource should be used – a somewhat tedious exploration of the modules’ GitHub documentations is often necessary.
Searching for the rule name on the useful site admx.help can give an indication of which values should be set in the configuration:

Using this information, a configuration can be written as so:
Configuration ConveniencePinDisabled {
Import-DSCResource -ModuleName 'PSDscResources'
Node localhost {
Registry "Ensure Turn on convenience PIN sign-in is set to Disabled" {
Key = 'HKLM:SoftwarePoliciesMicrosoftWindowsSystem'
ValueName = 'AllowDomainPINLogon'
ValueType = 'Dword'
ValueData = 0 # Corresponds to ‘Disabled’
Ensure = 'Present'
}
}
}
ConveniencePinDisabled
This should be saved as ‘ConveniencePinDisabled.ps1’, or whatever the configuration is named.
3 – Compile and Package the Configuration
The Configuration must be compiled and packaged into a ‘Machine Configuration Package Artifact’ which will be referenced by the custom Azure Policy. This will contain the compiled configuration as well as any necessary modules.
3.1. Compile the Configuration to a ‘.mof’ file
This step is performed simply by executing the following:
.<YourConfigurationName>.ps1
If successful, this will result in the creation of a file called localhost.mof within the directory <YourConfigurationFileName>.

Rename localhost.mof to <YourConfigurationFileName>.mof.
3.2. Create the Package Artifact
The cmdlet New-GuestConfigurationPackage is then used to create the artifact. Save and execute the following:
# Create a new Guest Configuration package
$params = @{
Name = "<YourConfigurationFileName>"
Configuration = "<PathToMofFile>"
Type = "AuditAndSet" # Can also be 'Audit'
Force = $true
}
New-GuestConfigurationPackage @params
Notice that the package Type is AuditAndSet. This will allow the package to provide remediation capability as well as auditing. If audit-only is required, set this to Audit.

4 – Upload the Package Artifact to Storage
The created artifact must be made accessible by the policy which will be created shortly. The example given here achieves this by uploading it to Azure Blob storage, in a dedicated container. The container will be set to be publicly readable so that published policies can always access any packages without any need for access tokens.
While it’s not generally advisable to store data in public containers, it’s not an issue in this case as the package artifacts do not represent sensitive data.
4.1. Create a Storage Account
Feel free to skip to step 4.3 if a suitable storage account and container already exists.
To create a storage account via the Azure portal, navigate to Storage Accounts -> Create and choose the Subscription, Resource group, name and region. Nothing else has to be configured at this point.

Select Review -> Create.
4.2. Create a Container
Navigate to the Storage Account -> Containers -> +Container and create a container named package-artifacts.
4.3. Allow Anonymous Blob Access
Again, within the Storage Account, Go to Configuration under Settings section and enable the Allow Blob Anonymous Access option and click save.

Finally, go back to Containers -> package-artifacts -> Change access level and select Blob (anonymous read access for blobs only) -> OK

4.4. Upload the Package to Storage
Replace the placeholder values in the following and run it as a PowerShell script from the same location as the created Package Artifact:
# Connect to Azure
Connect-AzAccount -Tenant '<YourTenantName>'
# Get Azure Storage context from an existing storage account
$StorageAccount = Get-AzStorageAccount -ResourceGroupName '<YourResourceGroupName>' -Name '<YourStorageAccountName>'
if ($StorageAccount) {
$context = $StorageAccount.Context
$setParams = @{
Container = 'package-artifacts'
File = '<YourConfigurationName>.zip'
Context = $context
}
# Upload package to storage
$blob = Set-AzStorageBlobContent @setParams
# Get the URI for the uploaded package
$contentUri = $blob.ICloudBlob.Uri.AbsoluteUri
Write-Host "File uploaded successfully to $contentUri"
} else {
Write-Host "Storage account $StorageAccountName not found in resource group $ResourceGroupName"
}
The URI needed to access the blob can then be referenced by the next code block as $contentUri.

5 – Create a Local Policy Definition
Create a folder called ‘policies’ in the working directory.
From the output of the above script, copy the content URI and add it to the script below. Also fill in the other placeholders to set a desired Policy display name, description and mode before running.
Important: the chosen mode will determine whether the Policy has remediation capability. Ensure that the value is compatible with the package Type set in step 3.2. For more information on Policy modes, refer to this remediation options article.
if (!(Get-Module "Az.Storage")) {
Write-Output 'Importing Module Az.Storage'
Install-Module -Name Az.Storage -Repository PSGallery -Force
Get-Module -ListAvailable -Name Az.Storage -Refresh
}
# generate new GUID
Write-Output 'Generating new GUID'
$guid = [guid]::NewGuid().ToString()
# create policy definition and save locally
$PolicyConfig = @{
PolicyId = $guid
ContentUri = '<ContentURI>'
DisplayName = '<YourDisplayName>'
Description = '<YourDescription>'
Path = './policies'
Platform = 'Windows'
Mode = '<PolicyMode>' # Audit, ApplyAndMonitor, or ApplyAndAutoCorrect
PolicyVersion = '1.0.0'
}
Write-Output ''
Write-Output 'Creating policy definition'
New-GuestConfigurationPolicy @PolicyConfig -Verbose

6 – Publish the Policy Definition to Azure
If the above script succeeds, a new policy definition will have been added to the policies folder. Add its filename to line 3 of the script below and set a name for the Policy before running.
# Publish policy definition to Azure
$JsonPath = '.policies<PolicyFileName>.json'
$PolicyJson = Get-Content -Path $JsonPath -Raw
Write-Output ''
Write-Output 'Publishing policy definition to Azure'
New-AzPolicyDefinition -Name '<YourPolicyName>' -Policy $PolicyJson -Verbose

7 – Assign the Policy
The newly created Policy can now be viewed by heading to Policy -> Definitions on the Azure Portal.

Assignments can be made by clicking on the Policy name -> Assign and selecting a scope. Toggling Policy enforcement between Enabled/Disabled will determine whether remediation can occur, if the Policy was not created in Audit mode. Set a non-compliance message and click Review + create.

8 – View Compliance
Finally, compliance can be viewed under the Policy assignment’s compliance tab.

By navigating to Home -> Guest Assignments it is possible to view the compliance of each machine with detailed reasons given.
E.g., for a non-compliant resource:

The same page, after running remediation:

Authors: Tom Johnson, Cal Grimes

