Azure Local - WDAC - Part 6 - Sign policies to protect against tampering


Intro

This article is part of a series: Navigate to series page

The earlier parts of this series covered creating, deploying, and updating supplemental WDAC policies. All of those policies were unsigned. An unsigned policy can be modified or removed by any administrator — including malware running with admin privileges.

Signed WDAC policies raise the bar significantly. Windows detects any tampering and responds with a boot failure (BSOD) rather than silently allowing a policy bypass. Once a signed policy is applied and the node reboots, it cannot be removed or overwritten without the signing certificate.

This part shows how to sign your base and supplemental policies and what you must prepare before enabling signature protection.

Warning: A misconfigured signed policy can cause a boot loop. Always validate on a single node first and keep an emergency recovery path available (e.g., BitLocker recovery key access and ability to boot from WinPE).

Prerequisites

  • A code signing certificate with an RSA key of 2048, 3072, or 4096 bits. ECDSA is not supported.
  • SHA-256 as the digest algorithm (SHA-384 and SHA-512 are supported on Windows 11 / Server 2022 with November 2022 CU or later).
  • The certificate must be signed according to the PKCS #7 standard (RFC 5652).
  • signtool.exe from the Windows SDK, accessible on the machine where you sign.
  • SecureBoot enabled on the Azure Local nodes (required for anti-tampering protection to take effect after reboot).
  • An existing unsigned base policy and any supplemental policies you want to protect.

Understand the signing requirement

When you sign a base policy, all supplemental policies must also be signed. The base policy must explicitly list each supplemental signing certificate via a <SupplementalPolicySigner> rule.

If you sign the base without also signing the supplementals, Windows will not load the unsigned supplementals after the reboot that activates signature enforcement.

Step 1 — Add boot safety options to the policy

Before converting to a signed policy, enable two safety options that give you a recovery path if the policy causes a boot issue:

# The policy you are about to sign
$policyPath = "C:\wdac\BasePolicy.xml"

# Option 9: Show advanced boot menu (lets you boot with WDAC disabled for troubleshooting)
Set-RuleOption -FilePath $policyPath -Option 9

# Option 10: Boot audit on failure instead of hard block
Set-RuleOption -FilePath $policyPath -Option 10

Keep these options in place until you have fully validated the signed policy in production. You can remove them in a later version increment once you are confident.

Step 2 — Add an UpdatePolicySigner rule

An <UpdatePolicySigner> rule tells Windows which certificate is allowed to update or replace this signed policy. This step is mandatory. If you skip it, you will not be able to update or remove the policy after signing, and the only recovery is a WinPE/BitLocker recovery path.

$policyPath   = "C:\wdac\BasePolicy.xml"
$certPath     = "C:\certs\MyWdacSigningCert.cer"  # exported public certificate (.cer)

# -Update allows this cert to replace the base policy
# -Supplemental allows this cert to sign supplemental policies for this base
Add-SignerRule -FilePath $policyPath -CertificatePath $certPath -Update -Supplemental

Export your signing certificate from your PKI or personal certificate store:

# Export from the personal store (if imported as .pfx)
$cert = Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Subject -match "WdacSigning" }
Export-Certificate -Cert $cert -FilePath "C:\certs\MyWdacSigningCert.cer"

Step 3 — Remove the unsigned policy option

Remove Option 6 (Unsigned System Integrity Policy). This tells Windows that the policy must be signed from this point on:

Set-RuleOption -FilePath $policyPath -Option 6 -Delete

Step 4 — Convert the policy to binary

$policyPath  = "C:\wdac\BasePolicy.xml"

$policyID    = Set-CIPolicyIdInfo -FilePath $policyPath -ResetPolicyID
$policyID    = $policyID.Substring(11)
$cipBinPath  = "C:\wdac\$policyID.cip"

ConvertFrom-CIPolicy -XmlFilePath $policyPath -BinaryFilePath $cipBinPath

Step 5 — Sign the binary policy

Import your .pfx signing certificate into the current user’s personal store if it is not already there, then sign the binary policy using signtool.exe:

# Adjust the path to signtool.exe based on your Windows SDK installation
$signtool = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe"

& $signtool sign `
  -v `
  -n "MyWdacSigningCert" `    # Subject name of certificate in personal store
  -p7 . `                     # PKCS#7 output to current directory
  -p7co 1.3.6.1.4.1.311.79.1 `
  -fd sha256 `
  $cipBinPath

This produces a file with a .p7 extension. Rename it to match the policy GUID:

Rename-Item -Path "C:\wdac\$policyID.cip.p7" -NewName "$policyID.cip"

Step 6 — Verify the signature

Before deploying, confirm the signature algorithm and chain are correct:

certutil.exe -asn "C:\wdac\$policyID.cip"

Look for sha256 in the signatureAlgorithm field. If you see sha1, the certificate or signing command needs adjustment.

Step 7 — Deploy the signed policy

Copy the signed .cip file to the Azure Local policy store and refresh:

$cipDest = "C:\Windows\System32\CodeIntegrity\CiPolicies\Active\$policyID.cip"
Copy-Item -Path "C:\wdac\$policyID.cip" -Destination $cipDest -Force

# Refresh policies without a reboot (where supported)
& C:\Windows\System32\RefreshPolicy.exe

Alternatively, use the Azure Local orchestrated cmdlet if you are deploying a supplemental policy:

Add-ASWDACSupplementalPolicy -Path "C:\wdac\SupplementalPolicy.xml"

Note: Anti-tampering protection only activates after the first reboot following deployment of the signed policy.

Step 8 — Validate on a single node before cluster-wide rollout

After deploying on one node, reboot and verify:

  1. The node boots successfully.

  2. WDAC policy mode is still Enforced:

    Get-AsWdacPolicyMode
  3. The signed policy appears in the active policy list:

    Get-ASLocalWDACPolicyInfo
  4. Applications covered by supplemental policies still run as expected.

Only proceed to the remaining nodes after confirming the above on the first node.

Signing supplemental policies

Each supplemental policy must also be signed with the same certificate (or a certificate listed in <SupplementalPolicySigner> rules of the base).

Follow the same steps 3–7 for each supplemental policy XML. The Add-SignerRule step with -Update -Supplemental is only needed on the base policy.

What happens if something goes wrong

If a signed policy causes a boot failure, recovery requires:

  1. Boot from WinPE or recovery environment.
  2. Use BitLocker recovery key to access the system volume.
  3. Delete or replace the .cip file in C:\Windows\System32\CodeIntegrity\CiPolicies\Active\.

This is why enabling Options 9 and 10 before signing is critical. With Option 9 active, you can access the advanced boot menu and select “Disable Windows Defender Application Control Policy” to get the system running while you fix the policy.

Recommendation

Treat signing as the final hardening step — do it after your unsigned policies have been stable and validated in production for at least one update cycle. Keep the signing certificate in a protected location (ideally an HSM or an offline PKI), and document the recovery procedure in your operational runbook.

Official Microsoft reference: Use signed policies to protect App Control for Business against tampering