Creating and Enforcing a Signed WDAC Policy with PowerShell
- Tenaka

- 2 days ago
- 12 min read
Windows Defender Application Control (WDAC) was previously known as DeviceGuard, now it's named App Control for Business. It is one of the strongest application-control technologies available in Windows. From this point forward, it will be referred to as WDAC.
Creating a basic WDAC policy is relatively straightforward. Creating a signed WDAC policy that boots correctly, survives administrative tampering and can still be safely updated is considerably more challenging. I've lost count of the Windows systems that failed to survive signing and ended with a BSoD.
The problem with devising a signed policy is that the information is spread across separate pages covering policy creation, rule options, certificate signing, deployment, Secure Boot and policy removal. Turning those individual components into a reliable end-to-end process required a fair amount of effort and trial and error, heavy on the error.
This article describes the lab process used to:
Scan a Windows 11 reference system.
Create and test an audit policy.
Convert it to unsigned enforcement.
Add an authorised policy update signer.
Sign the policy using SignTool.
Deploy it to Windows and the EFI system partition.
Confirm that removing the Windows policy file does not simply disable enforcement.
Important: This is a lab proof of concept. The scripts and procedures described here are not currently suitable for an enterprise production deployment.
Download and prepare the lab files
Download the WDAC signing lab from:
Extract the archive and copy the included subdirectories into:
C:\WDACThe completed folder structure should contain the numbered scripts and the included Windows SDK signing files.
Open Windows PowerShell or PowerShell ISE as an administrator, then run the scripts in numerical order, with reboots in between each script.
When using the standard PowerShell, set the execution policy for the current lab session:
Set-ExecutionPolicy -ExecutionPolicy Bypass -ForceThe repository includes the required SignTool.exe files. During the setup stage, these files are copied into the path expected by the signing scripts.
Create the destination directory:
New-Item `
-Path 'C:\Program Files (x86)\Windows Kits\10\bin\64' `
-ItemType Directory `
-ForceCopy the included signing tools:
Copy-Item `
-Path 'C:\WDAC\10.0.22621.0\x64\*' `
-Destination 'C:\Program Files (x86)\Windows Kits\10\bin\64' `
-Recurse `
-ForceOnce the files are in place, the lab scripts can locate and use SignTool.exe without requiring the full Windows SDK to be installed.
What an unsigned WDAC policy does
A WDAC policy enforces application-control and code-integrity rules that determine which kernel-mode drivers and user-mode code, including executables, DLLs, supported scripts and Windows Installer files, are permitted to run.
These rules are deployed as compiled binary policy files. For this article, we will use the modern multiple-policy format, saved as {PolicyGUID}.cip. This format is used by current Windows 11 deployments and supports multiple base and supplemental policies, clearer policy identification and modern management through tools such as CiTool.exe.
The older SiPolicy.p7b format is primarily associated with legacy single-policy deployments, older Windows versions and Group Policy-based management. The .cip format is not inherently more secure; it is used here because it provides the more flexible and current WDAC policy-management model.
In the lab, the initial policy is generated by scanning the complete Windows system drive:
New-CIPolicy `
-ScanPath 'C:\' `
-UserPEs `
-MultiplePolicyFormat `
-FilePath $AuditXml `
-Level FilePublisher `
-Fallback SignedVersion,Publisher,HashThe main rule level is FilePublisher, with fallbacks to:
SignedVersion
Publisher
Hash
This provides a useful balance for a lab system. Publisher-based rules are preferred where possible, while hashes provide a final fallback for unsigned or unusual files.
The -UserPEs option includes user-mode executable files and enables user-mode code integrity, or UMCI.
The XML policy is then converted into a deployable .cip file:
ConvertFrom-CIPolicy `
-XmlFilePath $AuditXml `
-BinaryFilePath $PolicyCipThe policy is deployed using CiTool.exe:
CiTool.exe --update-policy $PolicyCip -jsonMicrosoft documents CiTool.exe as the command-line utility for listing, updating and removing multiple-policy-format App Control policies.
Audit mode comes first
The first version of the lab policy is created in audit mode.
Rule option 3 enables audit behaviour:
Set-RuleOption -FilePath $AuditXml -Option 3In audit mode, Windows records applications that would have been blocked, but it does not prevent them from running.
This stage is critical. A filesystem scan only finds files that exist when the scan takes place. It does not automatically account for every file that might later be:
Downloaded by an application.
Extracted from an installer
Created during an update.
Loaded from a temporary directory.
Unpacked from a driver package (HP Universal Drivers on first use).
Before moving to enforcement, every required application, service, script and administrative tool should be launched, including HP printers with their first-use universal printer cab drivers.
The lab review script collects relevant events from:
Microsoft-Windows-CodeIntegrity/OperationalThe audit review includes event IDs:
3076
3089
3099
For example:
Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-CodeIntegrity/Operational'
Id = 3076,3089,3099
StartTime = (Get-Date).AddDays(-1)
}Any required application appearing in the audit events must be resolved before enforcement is enabled.
Moving to unsigned enforcement
Once the audit events have been reviewed, the policy is copied and its version is increased:
Copy-Item $AuditXml $EnforcedXml -Force
Set-CIPolicyVersion `
-FilePath $EnforcedXml `
-Version '1.0.1.0'Audit mode is then removed:
Set-RuleOption `
-FilePath $EnforcedXml `
-Option 3 `
-DeleteThe policy is converted again and redeployed with CiTool.exe.
After restarting, applications that are not authorised by the policy are blocked.
At this point, WDAC is enforcing application control, but the policy is still unsigned. Also at this point PowerShell will be in 'Constrained Language Mode' with unsigned scripts failing to load.
The weakness of an unsigned WDAC policy
An unsigned WDAC policy can provide effective application control, but it does not protect the policy itself.
Rule option 6 is present by default:
Enabled:Unsigned System Integrity PolicyThis option allows Windows to load the policy without a digital policy signature. Microsoft states that when option 6 is removed, the policy must be signed and its trusted update certificates must be defined in the UpdatePolicySigners section.
The weakness is that a process running with sufficient administrative privileges can replace or remove any unsigned policy.
The policy might successfully block users from running unauthorised software, but an administrator is able to remove the policy and eliminate its enforcement.
Microsoft claims that unsigned policies allow malware to modify or remove WDAC, but that oversimplifies reality:
WDAC is designed to stop untrusted code before it runs. In most cases, malware cannot simply launch a payload and delete the policy because the malware itself is blocked by the policy.
The greater risk is administrative abuse.
If an attacker gains local administrator or SYSTEM-level access, they may not need to run an obviously malicious executable. Instead, they can abuse trusted, Microsoft-signed tools and administrative interfaces already allowed by the system. This is commonly known as a Living off the Land technique.
Tools such as CiTool.exe, PowerShell or other trusted management components may be used to modify or remove an unsigned WDAC policy through legitimate administrative mechanisms.
An unsigned WDAC policy is therefore highly effective at stopping unauthorised applications and commodity malware. What it does not fully protect against is a trusted administrator—or an attacker operating with equivalent privileges—deliberately disabling the policy.
In simple terms:
An unsigned WDAC policy controls applications, but it does not securely control the administrator who owns the machine.
For lightly managed systems this may be acceptable. For a locked-down system where the application-control policy must resist local administrative tampering, the policy needs to be signed.
What changes when the WDAC policy is signed
Signing the policy is not the same as signing an executable file:
A signed WDAC policy introduces an authorised policy-update chain.
The installed policy identifies one or more certificates that are allowed to sign future updates.
Windows can then reject policy replacements that have not been signed by an authorised certificate.
Microsoft describes signed App Control policies as providing its highest level of policy protection and helping prevent policy tampering or removal, including by an administrator.
This changes the security model:
Unsigned policy
Windows loads the policy without a policy signature.
The policy can enforce executable, script and driver rules.
An administrator can remove or replace the policy.
Removing the active policy removes its enforcement.
Signed policy
Windows validates the policy’s PKCS#7 signature.
Future policy updates must be signed by an authorised update signer.
An arbitrary unsigned replacement policy is rejected.
The policy can be protected through the Secure Boot and EFI boot process.
Improper removal can cause a boot failure rather than cleanly disabling enforcement.
Lab prerequisites
The working lab uses:
Windows 11.
UEFI firmware.
Secure Boot enabled.
Windows PowerShell 5.1.
The built-in ConfigCI PowerShell module.
CiTool.exe.
A code-signing certificate.
Microsoft SignTool, seperate download.
A disposable virtual machine or test system.
At least one checkpoint taken before signed enforcement.
The script stops immediately if Secure Boot is not enabled:
if (-not (Confirm-SecureBootUEFI)) {
throw 'Secure Boot is not enabled.'
}It also verifies that it is running in Windows PowerShell rather than PowerShell 7:
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw 'Run this procedure from Windows PowerShell 5.1.'
}SignTool is required
Microsoft SignTool is required to produce the signed WDAC policy.
SignTool is included with the Windows SDK and is installed under the SDK Bin directory. The exact path varies depending on the SDK version and installed components.
The lab uses:
C:\Program Files (x86)\Windows Kits\10\bin\64\signtool.exeThe initial scan script checks for SignTool before beginning the full C:\ scan:
if (-not $SignTool -or
-not (Test-Path -LiteralPath $SignTool -PathType Leaf)) {
throw 'SignTool.exe was not found. Install the Windows SDK Signing Tools before scanning C:\.'
}This prevents the system from completing the long scan and audit process only to discover later that the required signing tool is unavailable.
Creating the lab signing certificate
The lab creates a self-signed code-signing certificate in the Local Machine certificate store:
$Certificate = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject 'CN=Tenaka WDAC Lab Policy Signer' `
-FriendlyName 'Tenaka WDAC Lab Policy Signer' `
-CertStoreLocation 'Cert:\LocalMachine\My' `
-KeyAlgorithm RSA `
-KeyLength 3072 `
-HashAlgorithm SHA256 `
-KeyExportPolicy Exportable `
-Provider 'Microsoft Software Key Storage Provider' `
-NotAfter (Get-Date).AddYears(5)The public certificate is exported as a .cer file:
Export-Certificate `
-Cert $Certificate `
-FilePath $CerPath `
-ForceThe certificate and private key are also backed up as a password-protected .pfx file:
Export-PfxCertificate `
-Cert $Certificate `
-FilePath $PfxPath `
-Password $PfxPassword `
-ChainOption EndEntityCertOnly `
-CryptoAlgorithmOption AES256_SHA256 `
-ForceThe script validates:
The private key exists.
The key uses RSA.
The RSA key size is supported.
The certificate contains the Code Signing EKU.
The exported public certificate matches the certificate containing the private key.
The Code Signing EKU is:
1.3.6.1.5.5.7.3.3This is acceptable for a controlled lab. It is not how an enterprise signing key should normally be managed.
Adding the authorised update signer
The unsigned enforced policy is copied to create the signed-policy XML:
Copy-Item $EnforcedXml $SignedXml -ForceThe certificate is then added as an update signer:
Add-SignerRule `
-FilePath $SignedXml `
-CertificatePath $CerPath `
-UpdateThe -Update switch is important. It creates an entry in the policy’s UpdatePolicySigners section.
This tells Windows which certificate is authorised to sign future policy versions.
The script validates that at least one update signer exists:
$UpdateSignerCount = @(
$SignedPolicyDocument.SiPolicy.UpdatePolicySigners.UpdatePolicySigner
).Count
if ($UpdateSignerCount -lt 1) {
throw 'No UpdatePolicySigner was found.'
}Signing a policy without correctly configuring its update signer can make future policy updates or removal extremely difficult.
Removing unsigned-policy support
The policy version is increased again:
Set-CIPolicyVersion `
-FilePath $SignedXml `
-Version '1.0.2.0'Rule option 6 is then removed:
Set-RuleOption `
-FilePath $SignedXml `
-Option 6 `
-DeleteRemoving option 6 changes the policy from a policy that may be unsigned into one that must be signed.
Microsoft documents that removing option 6 requires the policy and any applicable supplemental policies to be signed. It also requires trusted future update certificates to be defined in the policy.
The lab also confirms that supplemental policies are not enabled:
Set-RuleOption `
-FilePath $SignedXml `
-Option 17 `
-Delete `
-ErrorAction SilentlyContinueThis proof of concept uses a single base policy rather than a base-and-supplemental-policy design.
Lab recovery options
Two recovery-related policy options are retained:
Set-RuleOption -FilePath $SignedXml -Option 9
Set-RuleOption -FilePath $SignedXml -Option 10These represent:
Enabled:Advanced Boot Options Menu
Enabled:Boot Audit on FailureOption 9 allows the advanced boot menu to be made available to a physically present user.
Option 10 allows Windows to switch the policy into audit mode when a boot-critical driver is blocked, helping the operating system start instead of immediately failing.
Microsoft documents both behaviours in its policy rule-option reference.
These options are useful during development and testing. Their use in a production security baseline should be deliberately reviewed.
Converting and signing the policy
The signed XML is first converted into a binary .cip file:
ConvertFrom-CIPolicy `
-XmlFilePath $SignedXml `
-BinaryFilePath $PolicyCipSignTool then generates a detached PKCS#7 signature.
The WDAC policy content OID is:
1.3.6.1.4.1.311.79.1The working signing command is:
& $SignTool sign `
/v `
/sm `
/s My `
/sha1 $CertificateThumbprint `
/fd SHA256 `
/p7 $SignedOutput `
/p7co 1.3.6.1.4.1.311.79.1 `
$PolicyCipThe relevant switches are:
/v enables verbose output.
/sm selects the Local Machine certificate store.
/s My selects the Personal certificate store.
/sha1 selects the exact certificate by thumbprint.
/fd SHA256 uses SHA-256 as the file digest algorithm.
/p7 creates detached PKCS#7 output.
/p7co specifies the WDAC policy content OID.
Microsoft’s signed-policy procedure uses SignTool with /p7, the WDAC content OID and a supported digest algorithm.
SignTool creates a file named using the original filename with .p7 appended:
{PolicyID}.cip.p7The script copies that signed output into the final policy directory and renames it to:
{PolicyID}.cipThe .cip extension does not mean the file is still the original unsigned binary. The final file contains the PKCS#7-signed policy content.
Verifying the signed policy
The lab doesn't assume that a successful SignTool exit code is sufficient.
The public certificate is temporarily trusted on the lab system, and SignTool is used to verify the PKCS#7 signature:
& $SignTool verify `
/p7 `
/v `
/debug `
$FinalSignedCipThe policy is also parsed using:
certutil.exe -asn $FinalSignedCipFinally, its SHA-256 hash is recorded:
Get-FileHash `
$FinalSignedCip `
-Algorithm SHA256This provides a known hash for the final signed policy before deployment.
Deploying the signed policy
The first signed policy is copied to the Windows Code Integrity policy directory:
C:\Windows\System32\CodeIntegrity\CiPolicies\ActiveThe script then mounts the EFI system partition and copies the same policy to:
EFI\Microsoft\Boot\CiPolicies\ActiveThe EFI partition is located using:
$EfiPartition = (
Get-Partition |
Where-Object IsSystem |
Select-Object -First 1
).AccessPaths[0]It is mounted with:
mountvol.exe C:\EFIMount $EfiPartitionThe signed policy is then copied into the EFI policy directory.
Microsoft identifies both the Windows Code Integrity directory and the EFI system partition as possible policy locations for multiple-policy-format App Control policies.
The script calculates the hash of:
The original signed policy.
The Windows copy.
The EFI copy.
Deployment is stopped if the three hashes do not match.
if (($SourceHash -ne $OsHash) -or
($SourceHash -ne $EfiHash)) {
throw 'Source, OS and EFI hashes do not match.'
}The EFI partition is unmounted and the computer is restarted.
What happened when the Windows policy was deleted
The most important lab result came after signed enforcement was confirmed.
The signed policy file was deleted from:
C:\Windows\System32\CodeIntegrity\CiPolicies\ActiveApplications that were not allowed by the policy continued to be blocked.
Deleting the Windows copy did not disable enforcement because the signed policy had also been deployed into the EFI system partition.
This demonstrates an important difference between removing a file and performing an authorised policy removal:
Deleting the operating-system copy of a signed WDAC policy is not the same as removing the active policy.
The policy remained available through:
EFI\Microsoft\Boot\CiPolicies\Activeand continued to participate in the Secure Boot-backed startup process.
This should not be interpreted as meaning that every possible deletion of every signed-policy copy will always leave Windows running normally.
The accurate conclusion is:
Removing only the Windows copy may not disable the policy when an EFI copy remains.
An unsigned replacement cannot simply overwrite the signed policy.
Future updates must be signed by an authorised update signer.
Improperly removing all copies of an active signed policy may result in a boot failure rather than a clean removal.
Microsoft specifically warns that signed policies must be removed through the supported replacement process.
How a signed policy is removed correctly
A signed base policy should not simply be deleted.
Microsoft’s documented removal process requires a signed replacement policy that uses the same policy identity and re-enables rule option 6.
At a high level:
Start with the current signed-policy XML.
Retain the same PolicyID.
Increase, or at least retain, the existing policy version.
Add option 6 back to the policy.
Retain the authorised update signer.
Convert the replacement policy to .cip.
Sign it using a certificate authorised by the existing policy.
Deploy the signed replacement.
Restart the computer.
Remove the policy using CiTool.exe or the appropriate policy-removal process.
The replacement is still signed, allowing the currently installed policy to authorise the update. However, the replacement re-enables unsigned-policy support.
After the required restart, the Secure Boot protection is deactivated and the policy can be removed normally.
Losing the signing private key could therefore create a serious recovery problem. The certificate, private key and recovery procedure must be protected before signed enforcement is deployed.
Post-signing validation
After restarting, the lab checks Secure Boot:
Confirm-SecureBootUEFIIt then records the active policies:
CiTool.exe --list-policies -jsonThe target policy should report values equivalent to:
IsSignedPolicy : True
IsEnforced : True
IsAuthorized : True
IsOnDisk : TrueThe post-signing review also collects events:
3077
3089
3099
Event 3077 is particularly important because it records code that was blocked while the policy was in enforcement mode.
The policy should then be tested across two complete shutdown and startup cycles.
Required applications should continue to work, while deliberately unauthorised applications should remain blocked.
Final thoughts
An unsigned WDAC policy does provide effective application control. It can block unauthorised executables, scripts, DLLs and drivers, but it does not fully protect the policy itself. A local administrator, is able to remove the policy and eliminate its enforcement.
Signing the WDAC policy changes that security model. The policy defines which certificate is authorised to approve future versions, and Windows validates the signed policy during startup. When the policy is also deployed to the EFI system partition, its enforcement becomes tied to the Secure Boot-backed startup process.
This additional protection does, however, introduce risk and administrative overhead.
The signing certificate and private key become critical security assets. They must be securely stored, backed up and available whenever the policy needs to be updated or removed. If the authorised signing key is lost, expired or unavailable, maintaining the policy can become extremely difficult.
Every policy change must also follow a controlled lifecycle. The policy version must be managed correctly, the update signer must remain valid, the replacement policy must be signed, and the deployment and recovery process must be tested before the change reaches production systems.
A mistake in an unsigned policy may block an application. A mistake in a signed policy can prevent the operating system from starting.
Signed WDAC therefore provides a much stronger application-control boundary, particularly on systems where local administrators should not be able to bypass enforcement. That protection comes at the cost of additional certificate management, change control, testing, recovery planning and operational responsibility.
For tightly controlled or high-security devices, that overhead may be justified. For general-purpose systems, an unsigned policy may provide a more practical balance between application control, supportability and recovery.




Comments