<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mimachniak.github.io/sysopslife/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mimachniak.github.io/sysopslife/" rel="alternate" type="text/html" /><updated>2026-07-21T23:50:21+02:00</updated><id>https://mimachniak.github.io/sysopslife/feed.xml</id><title type="html">AzOps Engineer</title><subtitle>Michal Machniak blog dedicated to Microsoft technologies, based on my daily experience of working as sysOps / devOps / System Architect.</subtitle><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><entry><title type="html">Azure Custom Guest Configuration with PowerShell DSC and Azure Guest Configuration.</title><link href="https://mimachniak.github.io/sysopslife/2026/07/21/azure-guest-configuration/" rel="alternate" type="text/html" title="Azure Custom Guest Configuration with PowerShell DSC and Azure Guest Configuration." /><published>2026-07-21T00:00:00+02:00</published><updated>2026-07-21T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2026/07/21/azure-guest-configuration</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2026/07/21/azure-guest-configuration/"><![CDATA[<h1 id="azure-custom-guest-configuration-with-powershell-dsc">Azure Custom Guest Configuration with PowerShell DSC</h1>

<p>Azure Guest Configuration extends Azure Policy into the operating system of Azure virtual machines and Arc-enabled servers. A custom guest configuration lets you describe a desired state in PowerShell DSC, package it, host it, and use Azure Policy to audit or remediate that state at scale.</p>

<p>This article walks through a Windows example that enforces a legal logon message. The same process applies to IIS features, Windows optional features, users, registry values, and other DSC resources.</p>

<blockquote>
  <p><strong>Important:</strong> Test every package in a non-production subscription first. An <code class="language-plaintext highlighter-rouge">AuditAndSet</code> package changes the guest operating system when it detects drift.</p>
</blockquote>

<hr />

<h1 id="matrix-of-supported-systems">Matrix of supported systems</h1>

<p>Machine configuration policy definitions are inclusive of new versions. Older versions of operating systems available in Azure Marketplace are excluded if the Guest Configuration client isn’t compatible. Additionally, Linux server versions that are out of lifetime support by their respective publishers are excluded from the support matrix.</p>

<p><img src="/assets/images/guest-config-p2.png" alt="" /></p>

<hr />

<h1 id="how-the-pieces-fit-together">How the pieces fit together</h1>

<p>The workflow has five parts:</p>

<ol>
  <li>Author a DSC configuration and compile it into a MOF file.</li>
  <li>Package the MOF with the Guest Configuration module.</li>
  <li>Validate the package locally and calculate its content hash.</li>
  <li>Upload the ZIP package to HTTPS-accessible blob storage.</li>
  <li>Assignee configuration package to Virtual Machine.</li>
</ol>

<hr />

<h1 id="prerequisites">Prerequisites</h1>

<p>Before creating a custom package, prepare the authoring workstation and Azure environment.</p>

<table>
  <thead>
    <tr>
      <th>Requirement</th>
      <th>Why it is needed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Windows authoring workstation</td>
      <td>This example uses Windows PowerShell DSC resources and creates a Windows MOF.</td>
    </tr>
    <tr>
      <td>PowerShell 7</td>
      <td>Recommended for building and testing Guest Configuration packages.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PSDscResources</code> module</td>
      <td>Supplies the DSC resources used by the configuration, such as <code class="language-plaintext highlighter-rouge">Registry</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GuestConfiguration</code> module</td>
      <td>Creates, validates, and turns a package into an Azure Policy definition.</td>
    </tr>
    <tr>
      <td>Azure PowerShell sign-in</td>
      <td>Required to upload content and deploy or assign the generated policy.</td>
    </tr>
    <tr>
      <td>HTTPS-accessible package URI</td>
      <td>Each target machine must be able to download the ZIP package from the URI. Azure Blob Storage is a common choice.</td>
    </tr>
    <tr>
      <td>Azure permissions</td>
      <td>Use a role that can create policy definitions and assignments, and can upload the package to the selected storage account.</td>
    </tr>
    <tr>
      <td>Supported Azure VM or Arc-enabled server</td>
      <td>The target must be able to receive the Guest Configuration extension and reach Azure Policy and the package URI.</td>
    </tr>
  </tbody>
</table>

<p>Install the two DSC modules on the authoring workstation:</p>

<blockquote>
  <p>Note: PSDesiredStateConfiguration is required in version at lest 2.0.7</p>
</blockquote>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">PSDscResources</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">CurrentUser</span><span class="w">
</span><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">GuestConfiguration</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">CurrentUser</span><span class="w"> </span><span class="c"># Work only on PowerShell 7</span><span class="w">

</span><span class="n">Get-Module</span><span class="w"> </span><span class="nt">-ListAvailable</span><span class="w"> </span><span class="nt">-FullyQualifiedName</span><span class="w"> </span><span class="nx">PSDscResources</span><span class="w">
</span><span class="n">Get-Module</span><span class="w"> </span><span class="nt">-ListAvailable</span><span class="w"> </span><span class="nt">-FullyQualifiedName</span><span class="w"> </span><span class="nx">GuestConfiguration</span><span class="w">
</span></code></pre></div></div>

<p>Sign in to the tenant and select the subscription where the policy will be assigned:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Connect-AzAccount</span><span class="w">
</span><span class="nx">Set-AzContext</span><span class="w"> </span><span class="nt">-Subscription</span><span class="w"> </span><span class="s1">'&lt;subscription-id-or-name&gt;'</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="step-1-virtual-machine-need-to-have-those-perquisites">Step 1: Virtual Machine need to have those perquisites</h2>

<p>⁉️ Resource provider on subscription need to be registred: Microsoft.GuestConfiguration. <br />
⁉️ System Assigne Identity / User Managed Idenity.<br />
⁉️ Virtual machine extension is enabled, To use machine configuration packages that apply configurations, Azure VM guest configuration extension version 1.26.24 or later, or Arc agent 1.10.0 or later, is required.<br />
⁉️ Azure Arc servers are supported.</p>

<p>Prerequisites can be added by Azure policy to all servers on different scope:</p>

<p><img src="/assets/images/guest-config-p5.png" alt="" /></p>

<hr />
<h2 id="step-2-author-and-compile-the-dsc-configuration">Step 2: Author and compile the DSC configuration</h2>

<p>Guest Configuration packages use a compiled DSC MOF. The following configuration writes a title and body for the Windows interactive logon message.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">Configuration</span><span class="w"> </span><span class="n">SetupLogonMessage</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Import-DscResource</span><span class="w"> </span><span class="nt">-ModuleName</span><span class="w"> </span><span class="nx">PSDscResources</span><span class="w">

    </span><span class="n">Node</span><span class="w"> </span><span class="s1">'localhost'</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Registry</span><span class="w"> </span><span class="nx">LogonMessageTitle</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Key</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'</span><span class="w">
            </span><span class="n">ValueName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'legalnoticecaption'</span><span class="w">
            </span><span class="n">ValueData</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Security Warning'</span><span class="w">
            </span><span class="n">ValueType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'String'</span><span class="w">
            </span><span class="n">Ensure</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'Present'</span><span class="w">
            </span><span class="n">Force</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
        </span><span class="p">}</span><span class="w">

        </span><span class="n">Registry</span><span class="w"> </span><span class="nx">LogonMessageBody</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Key</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="s1">'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'</span><span class="w">
            </span><span class="n">ValueName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'legalnoticetext'</span><span class="w">
            </span><span class="n">ValueData</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Authorized access only. All activities are monitored and recorded.'</span><span class="w">
            </span><span class="n">ValueType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'String'</span><span class="w">
            </span><span class="n">Ensure</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'Present'</span><span class="w">
            </span><span class="n">DependsOn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'[Registry]LogonMessageTitle'</span><span class="w">
            </span><span class="n">Force</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">SetupLogonMessage</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="o">.</span><span class="nx">\SetupLogonMessage</span><span class="w">
</span></code></pre></div></div>

<p>Compilation creates <code class="language-plaintext highlighter-rouge">SetupLogonMessage.mof</code> in the output folder. Keep the configuration source in version control alongside the generated policy files, but rebuild the MOF and package whenever the configuration changes.</p>

<hr />

<h2 id="step-3-create-the-guest-configuration-package">Step 3: Create the Guest Configuration package</h2>

<blockquote>
  <p>Note: PowerShell 7 need to be running as local administrator</p>
</blockquote>

<p>Choose the package type deliberately:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Behaviour</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Audit</code></td>
      <td>Reports whether the setting is compliant; it never changes the machine.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AuditAndSet</code></td>
      <td>Reports compliance and applies the DSC configuration when the machine is non-compliant.</td>
    </tr>
  </tbody>
</table>

<p>For the logon-message configuration, create an <code class="language-plaintext highlighter-rouge">AuditAndSet</code> package:</p>

<blockquote>
  <p>Note: Package name need to exactly the same as configuration file.</p>
</blockquote>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">Name</span><span class="w">          </span><span class="o">=</span><span class="w"> </span><span class="s1">'SetupLogonMessage'</span><span class="w">
    </span><span class="nx">Configuration</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'./SetupLogonMessage/SetupLogonMessage.mof'</span><span class="w">
    </span><span class="nx">Type</span><span class="w">          </span><span class="o">=</span><span class="w"> </span><span class="s1">'AuditAndSet'</span><span class="w">
    </span><span class="nx">Force</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
    </span><span class="c"># FrequencyMinutes = 180 # Default is 15 minutes.</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">New-GuestConfigurationPackage</span><span class="w"> </span><span class="err">@</span><span class="nx">params</span><span class="w">
</span></code></pre></div></div>

<p>Zip file will contain resources and config file.</p>

<p><img src="/assets/images/guest-config-p3.png" alt="" /></p>

<p><img src="/assets/images/guest-config-p4.png" alt="" /></p>

<hr />

<h2 id="step-4-validate-the-package-before-publishing">Step 4: Validate the package before publishing</h2>

<p>Validate locally before you place the ZIP in storage or create a policy definition. This catches packaging errors and gives you the hash that Azure Policy uses to identify the exact package version.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-FileHash</span><span class="w"> </span><span class="o">.</span><span class="nx">\SetupLogonMessage.zip</span><span class="w">
</span><span class="n">Get-GuestConfigurationPackageComplianceStatus</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="nx">\SetupLogonMessage.zip</span><span class="w">
</span></code></pre></div></div>

<p>The compliance command should return a result instead of a packaging or resource-loading error. If it fails, confirm that every DSC resource referenced by the MOF is available to the Guest Configuration packaging process and that the MOF path is correct.</p>

<hr />

<h2 id="step-5-publish-the-zip-to-a-stable-https-uri">Step 5: Publish the ZIP to a stable HTTPS URI</h2>

<p>Upload the ZIP to Azure Blob Storage. The policy definition must reference a URI that every target machine can reach over HTTPS. A blob URI with a time-limited SAS token is useful for testing, but production packages should use an access method and renewal process that will not unexpectedly expire.</p>

<p>After upload, keep the content URI and the SHA-256 value from <code class="language-plaintext highlighter-rouge">Get-FileHash</code> available. Treat a package update as a versioned release: publish a new ZIP, validate it, then update the policy definition or create a new policy version.</p>

<hr />

<h2 id="step-6-deploy-guest-configuration-to-one-virtual-machine">Step 6: Deploy guest configuration to one virtual machine</h2>

<p>For a single test VM, create a Guest Configuration assignment directly on the VM. This does not create an Azure Policy definition or policy assignment. Azure installs or updates the Guest Configuration extension as part of processing the assignment.</p>

<p>Install the Azure PowerShell module that provides the assignment cmdlets:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">Az.GuestConfiguration</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">CurrentUser</span><span class="w">
</span></code></pre></div></div>

<p>Set the values for the target VM and the published ZIP. The content hash must be the SHA-256 value returned in Step 4 for the exact ZIP file at <code class="language-plaintext highlighter-rouge">$contentUri</code>.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$resourceGroupName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;resource-group-name&gt;'</span><span class="w">
</span><span class="nv">$vmName</span><span class="w">            </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;virtual-machine-name&gt;'</span><span class="w">
</span><span class="nv">$contentUri</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://&lt;storage-account&gt;.blob.core.windows.net/&lt;container&gt;/SetupLogonMessage.zip?&lt;sas-token&gt;'</span><span class="w">
</span><span class="nv">$contentHash</span><span class="w">       </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-FileHash</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s1">'.\SetupLogonMessage.zip'</span><span class="w"> </span><span class="nt">-Algorithm</span><span class="w"> </span><span class="nx">SHA256</span><span class="p">)</span><span class="o">.</span><span class="nf">Hash</span><span class="w">

</span><span class="nv">$assignmentParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">GuestConfigurationAssignmentName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'SetupLogonMessage'</span><span class="w">
    </span><span class="nx">ResourceGroupName</span><span class="w">                </span><span class="o">=</span><span class="w"> </span><span class="nv">$resourceGroupName</span><span class="w">
    </span><span class="nx">VMName</span><span class="w">                           </span><span class="o">=</span><span class="w"> </span><span class="nv">$vmName</span><span class="w">
    </span><span class="nx">GuestConfigurationName</span><span class="w">           </span><span class="o">=</span><span class="w"> </span><span class="s1">'SetupLogonMessage'</span><span class="w">
    </span><span class="nx">GuestConfigurationVersion</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s1">'1.0.0'</span><span class="w">
    </span><span class="nx">GuestConfigurationContentUri</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$contentUri</span><span class="w">
    </span><span class="nx">GuestConfigurationContentHash</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="nv">$contentHash</span><span class="w">
    </span><span class="nx">GuestConfigurationAssignmentType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'ApplyAndAutoCorrect'</span><span class="w">
    </span><span class="nx">GuestConfigurationKind</span><span class="w">           </span><span class="o">=</span><span class="w"> </span><span class="s1">'DSC'</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">New-AzGuestConfigurationAssignment</span><span class="w"> </span><span class="err">@</span><span class="nx">assignmentParams</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ApplyAndAutoCorrect</code> applies the <code class="language-plaintext highlighter-rouge">AuditAndSet</code> package and corrects drift. To use an <code class="language-plaintext highlighter-rouge">Audit</code> package instead, set <code class="language-plaintext highlighter-rouge">GuestConfigurationAssignmentType</code> to <code class="language-plaintext highlighter-rouge">Audit</code>; the extension reports compliance but does not change the VM.</p>

<p>Configuration can be added by Azure portal, as well from Virtual machine management resources.</p>

<p><img src="/assets/images/guest-config-p6.png" alt="" /></p>

<p>Check the assignment status and verify the expected values on the VM:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-AzGuestConfigurationAssignment</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ResourceGroupName</span><span class="w"> </span><span class="nv">$resourceGroupName</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-VMName</span><span class="w"> </span><span class="nv">$vmName</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-GuestConfigurationAssignmentName</span><span class="w"> </span><span class="s1">'SetupLogonMessage'</span><span class="w">

</span><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Path</span><span class="w"> </span><span class="s1">'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">legalnoticecaption</span><span class="p">,</span><span class="w"> </span><span class="nx">legalnoticetext</span><span class="w">
</span></code></pre></div></div>

<p>Check assignment is also available  form Azure portal from Virtual Machine resource management plane.</p>

<p><img src="/assets/images/guest-config-p8.png" alt="" /></p>

<p><img src="/assets/images/guest-config-p9.png" alt="" /></p>

<blockquote>
  <p>Note: The assignment must show a successful provisioning state before the configuration can report compliance. Ensure the VM can reach the package URI over HTTPS and that the SAS token remains valid for the test.</p>
</blockquote>

<p>Guest Configuration assignment can be check globally for all resources in section <strong>Guest Assignments</strong> in Azure Portal</p>

<p><img src="/assets/images/guest-config-p10.png" alt="" /></p>

<hr />

<h1 id="dsc-guest-configuration-custom-package-authoring-and-deploying-process">DSC (Guest Configuration) custom package authoring and deploying process</h1>

<p><img src="/assets/images/guest-config-p7.png" alt="" /></p>

<h1 id="common-problems">Common problems</h1>

<table>
  <thead>
    <tr>
      <th>Symptom</th>
      <th>What to check</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Package creation fails</td>
      <td>Confirm the MOF exists and that required DSC modules are installed and importable.</td>
    </tr>
    <tr>
      <td>Local validation fails</td>
      <td>Run <code class="language-plaintext highlighter-rouge">Get-GuestConfigurationPackageComplianceStatus</code> from the package directory and resolve missing resource dependencies.</td>
    </tr>
    <tr>
      <td>Machines show no result</td>
      <td>Confirm the VM or Arc server is supported, connected, and able to receive the Guest Configuration extension.</td>
    </tr>
    <tr>
      <td>Package download fails</td>
      <td>Verify DNS, HTTPS egress, storage firewall rules, and that the package URI or SAS token remains valid.</td>
    </tr>
    <tr>
      <td>Unexpected remediation</td>
      <td>Confirm the package was built with <code class="language-plaintext highlighter-rouge">Audit</code>, not <code class="language-plaintext highlighter-rouge">AuditAndSet</code>, before assigning it broadly.</td>
    </tr>
    <tr>
      <td>A package update is ignored</td>
      <td>Increase the policy/package version and update the policy definition to point to the intended artifact.</td>
    </tr>
  </tbody>
</table>

<hr />

<h1 id="summary">Summary</h1>

<p>Custom Guest Configuration turns a DSC configuration into a control that can be measured across a fleet. The reliable loop is simple: compile the MOF, package it, validate it locally, publish it to a durable HTTPS location, generate the policy, and assign it to a limited test scope before expanding the rollout.</p>

<h1 id="examples-of-code">Examples of code</h1>

<p>Code examples of configuration can be found on this repo in GitHub - <a href="https://github.com/mimachniak/sysopslife-scripts/tree/master/DSC/2_0">Blog code examples Github</a></p>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="Azure" /><category term="DSC" /><category term="PowerShell" /><category term="Guest Configuration" /><category term="PowerShell DSC" /><category term="IaaC" /><summary type="html"><![CDATA[Build, test, publish, and assign a custom Azure Guest Configuration package using PowerShell DSC and Azure Policy.]]></summary></entry><entry><title type="html">How to Use DSC v3 Resources for Azure Arc and What Options Can Be Configured</title><link href="https://mimachniak.github.io/sysopslife/2026/06/22/azure-arc-dscv3-setup/" rel="alternate" type="text/html" title="How to Use DSC v3 Resources for Azure Arc and What Options Can Be Configured" /><published>2026-06-22T00:00:00+02:00</published><updated>2026-06-22T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2026/06/22/azure-arc-dscv3-setup</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2026/06/22/azure-arc-dscv3-setup/"><![CDATA[<h1 id="how-to-use-dsc-v3-resources-for-azure-arc-and-what-options-can-be-configured">How to Use DSC v3 Resources for Azure Arc and What Options Can Be Configured</h1>

<p>This guide shows how to discover DSC v3 resources for Azure Arc, how to list and filter resources available on your machine, and how to read current configuration state before applying changes.</p>

<p>The resource used in this catalog is <code class="language-plaintext highlighter-rouge">Microsoft.Azure.Arc/AgentConfiguration</code>.</p>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<p>Install DSC v3:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nt">--id</span><span class="w"> </span><span class="nx">Microsoft.DSC</span><span class="w"> </span><span class="nt">--exact</span><span class="w"> </span><span class="nt">--source</span><span class="w"> </span><span class="nx">winget</span><span class="w">
</span></code></pre></div></div>

<p>Make the resource discoverable:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$repoRoot</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"D:\Git\AzureConnectedMachineDsc"</span><span class="w">
</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">DSC_RESOURCE_PATH</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Join-Path</span><span class="w"> </span><span class="nv">$repoRoot</span><span class="w"> </span><span class="s1">'dsc_resources'</span><span class="w">
</span></code></pre></div></div>

<p>Confirm the resource is visible:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="s1">'Microsoft.Azure.Arc/AgentConfiguration'</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="how-to-list-and-get-dsc-resources">How to List and Get DSC Resources</h2>

<p>List all DSC resources currently available:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w">
</span></code></pre></div></div>

<p>List only Azure Arc-related resources:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="s1">'Microsoft.Azure.Arc'</span><span class="w">
</span></code></pre></div></div>

<p>Get current state from the Azure Arc resource:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">'{}'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">get</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">get</code> output includes an <code class="language-plaintext highlighter-rouge">actualState</code> object with values such as:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">incomingConnectionsEnabled</code></li>
  <li><code class="language-plaintext highlighter-rouge">guestConfigurationEnabled</code></li>
  <li><code class="language-plaintext highlighter-rouge">extensionsEnabled</code></li>
  <li><code class="language-plaintext highlighter-rouge">extensionAllowlist</code></li>
  <li><code class="language-plaintext highlighter-rouge">extensionBlocklist</code></li>
  <li><code class="language-plaintext highlighter-rouge">configMode</code></li>
  <li><code class="language-plaintext highlighter-rouge">agentInstalled</code></li>
</ul>

<p>Use this output as your baseline before creating desired-state payloads for <code class="language-plaintext highlighter-rouge">test</code> and <code class="language-plaintext highlighter-rouge">set</code>.</p>

<hr />

<h2 id="configurable-options">Configurable Options</h2>

<p>The resource supports these writable options:</p>

<table>
  <thead>
    <tr>
      <th>Property</th>
      <th>Type</th>
      <th>Allowed values</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">incomingConnectionsEnabled</code></td>
      <td>boolean or null</td>
      <td><code class="language-plaintext highlighter-rouge">true</code>, <code class="language-plaintext highlighter-rouge">false</code>, <code class="language-plaintext highlighter-rouge">null</code></td>
      <td>Enables or disables incoming connections for the Arc agent.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">guestConfigurationEnabled</code></td>
      <td>boolean or null</td>
      <td><code class="language-plaintext highlighter-rouge">true</code>, <code class="language-plaintext highlighter-rouge">false</code>, <code class="language-plaintext highlighter-rouge">null</code></td>
      <td>Enables or disables Guest Configuration.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">extensionsEnabled</code></td>
      <td>boolean or null</td>
      <td><code class="language-plaintext highlighter-rouge">true</code>, <code class="language-plaintext highlighter-rouge">false</code>, <code class="language-plaintext highlighter-rouge">null</code></td>
      <td>Enables or disables extension handling.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">extensionAllowlist</code></td>
      <td>array of string or null</td>
      <td>Any extension IDs</td>
      <td>Explicit list of allowed extensions.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">extensionBlocklist</code></td>
      <td>array of string or null</td>
      <td>Any extension IDs</td>
      <td>Explicit list of blocked extensions.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">configMode</code></td>
      <td>string or null</td>
      <td><code class="language-plaintext highlighter-rouge">monitor</code>, <code class="language-plaintext highlighter-rouge">full</code>, <code class="language-plaintext highlighter-rouge">null</code></td>
      <td>Agent configuration mode.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">proxyUrl</code></td>
      <td>string or null</td>
      <td>Any URL string or <code class="language-plaintext highlighter-rouge">null</code></td>
      <td>Proxy endpoint used by the agent.</td>
    </tr>
  </tbody>
</table>

<p>Read-only output fields:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">agentInstalled</code></li>
  <li><code class="language-plaintext highlighter-rouge">_inDesiredState</code></li>
</ul>

<p>Important runtime notes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">set</code> operation requires elevated security context.</li>
  <li>Unknown properties are rejected.</li>
  <li><code class="language-plaintext highlighter-rouge">configMode</code> only accepts <code class="language-plaintext highlighter-rouge">monitor</code> or <code class="language-plaintext highlighter-rouge">full</code>.</li>
</ul>

<hr />

<h2 id="quick-get-and-test">Quick Get and Test</h2>

<p>Get current state:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">'{}'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">get</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">
</span></code></pre></div></div>

<p>Test desired state:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$desired</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
	</span><span class="nx">incomingConnectionsEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">guestConfigurationEnabled</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">extensionsEnabled</span><span class="w">          </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">extensionAllowlist</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.AzureDefenderForServers/MDE.Windows"</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Monitor/AzureMonitorWindowsAgent"</span><span class="w">
	</span><span class="p">)</span><span class="w">
	</span><span class="nx">extensionBlocklist</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Automation.HybridWorker/HybridWorkerForWindows"</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Automation/HybridWorkerForLinux"</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Extensions/CustomScript"</span><span class="w">
		</span><span class="s2">"Microsoft.Cplat.Core/RunCommandHandlerLinux"</span><span class="w">
		</span><span class="s2">"Microsoft.Cplat.Core/RunCommandHandlerWindows"</span><span class="w">
		</span><span class="s2">"Microsoft.Compute/CustomScriptExtension"</span><span class="w">
		</span><span class="s2">"Microsoft.EnterpriseCloud.Monitoring/MicrosoftMonitoringAgent"</span><span class="w">
		</span><span class="s2">"Microsoft.EnterpriseCloud.Monitoring/OMSAgentForLinux"</span><span class="w">
	</span><span class="p">)</span><span class="w">
	</span><span class="nx">configMode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"full"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$desired</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="nt">-Compress</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">ConvertFrom-Json</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="example-export-current-configuration">Example: Export Current Configuration</h2>

<p>Use <code class="language-plaintext highlighter-rouge">export</code> to capture live, non-null settings and reuse them as a baseline:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$exported</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'{}'</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">export</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">ConvertFrom-Json</span><span class="w">

</span><span class="nv">$exported</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Set-Content</span><span class="w"> </span><span class="o">.</span><span class="nx">\arc-agent-export.json</span><span class="w">
</span></code></pre></div></div>

<p>Why this helps:</p>

<ul>
  <li>Baselines current production settings.</li>
  <li>Makes onboarding additional servers easier.</li>
  <li>Supports drift investigations.</li>
</ul>

<hr />

<h2 id="tier-0-server-baseline-recommended">Tier 0 Server Baseline (Recommended)</h2>

<p>For Tier 0 servers, use a strict Arc agent profile with the following requirements:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">incomingconnections.enabled</code> = <code class="language-plaintext highlighter-rouge">false</code></li>
  <li><code class="language-plaintext highlighter-rouge">guestconfiguration.enabled</code> = <code class="language-plaintext highlighter-rouge">false</code></li>
  <li><code class="language-plaintext highlighter-rouge">extensions.allowlist</code> = <code class="language-plaintext highlighter-rouge">Microsoft.Azure.Monitor/AzureMonitorWindowsAgent,Microsoft.Azure.AzureDefenderForServers/MDE.Windows</code></li>
</ul>

<p>In the DSC resource payload, these map to:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">incomingConnectionsEnabled = $false</code></li>
  <li><code class="language-plaintext highlighter-rouge">guestConfigurationEnabled = $false</code></li>
  <li><code class="language-plaintext highlighter-rouge">extensionAllowlist = @("Microsoft.Azure.Monitor/AzureMonitorWindowsAgent", "Microsoft.Azure.AzureDefenderForServers/MDE.Windows")</code></li>
</ul>

<p>Tier 0 set example:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$tier0Desired</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
	</span><span class="nx">incomingConnectionsEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">guestConfigurationEnabled</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">extensionAllowlist</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Monitor/AzureMonitorWindowsAgent"</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.AzureDefenderForServers/MDE.Windows"</span><span class="w">
	</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$tier0Desired</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="nt">-Compress</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">ConvertFrom-Json</span><span class="w">

</span><span class="nv">$tier0Desired</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="nt">-Compress</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">ConvertFrom-Json</span><span class="w">
</span></code></pre></div></div>

<p>This baseline reduces attack surface by disabling incoming Arc connections, turning off guest configuration, and allowing only the required monitoring and defender extensions.</p>

<hr />

<h2 id="example-set-desired-configuration">Example: Set Desired Configuration</h2>

<p>Apply a hardened configuration with allowlist/blocklist control and full mode:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$desired</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
	</span><span class="nx">incomingConnectionsEnabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">guestConfigurationEnabled</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">extensionsEnabled</span><span class="w">          </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
	</span><span class="nx">extensionAllowlist</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.AzureDefenderForServers/MDE.Windows"</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Monitor/AzureMonitorWindowsAgent"</span><span class="w">
	</span><span class="p">)</span><span class="w">
	</span><span class="nx">extensionBlocklist</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Automation.HybridWorker/HybridWorkerForWindows"</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Automation/HybridWorkerForLinux"</span><span class="w">
		</span><span class="s2">"Microsoft.Azure.Extensions/CustomScript"</span><span class="w">
		</span><span class="s2">"Microsoft.Cplat.Core/RunCommandHandlerLinux"</span><span class="w">
		</span><span class="s2">"Microsoft.Cplat.Core/RunCommandHandlerWindows"</span><span class="w">
		</span><span class="s2">"Microsoft.Compute/CustomScriptExtension"</span><span class="w">
		</span><span class="s2">"Microsoft.EnterpriseCloud.Monitoring/MicrosoftMonitoringAgent"</span><span class="w">
		</span><span class="s2">"Microsoft.EnterpriseCloud.Monitoring/OMSAgentForLinux"</span><span class="w">
	</span><span class="p">)</span><span class="w">
	</span><span class="nx">configMode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"full"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$desired</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="nt">-Compress</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">ConvertFrom-Json</span><span class="w">
</span></code></pre></div></div>

<p>After <code class="language-plaintext highlighter-rouge">set</code>, run a validation pass:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$desired</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="nt">-Compress</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nt">-r</span><span class="w"> </span><span class="nx">Microsoft.Azure.Arc/AgentConfiguration</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o">|</span><span class="w">
	</span><span class="n">ConvertFrom-Json</span><span class="w">
</span></code></pre></div></div>

<p>You should see <code class="language-plaintext highlighter-rouge">_inDesiredState: true</code> when everything is applied correctly.</p>

<hr />

<h2 id="troubleshooting-tips">Troubleshooting Tips</h2>

<p>If DSC cannot find the resource:</p>

<ul>
  <li>Verify <code class="language-plaintext highlighter-rouge">DSC_RESOURCE_PATH</code> points to the <code class="language-plaintext highlighter-rouge">dsc_resources</code> folder.</li>
  <li>Run <code class="language-plaintext highlighter-rouge">dsc resource list</code> and check for <code class="language-plaintext highlighter-rouge">Microsoft.Azure.Arc/AgentConfiguration</code>.</li>
</ul>

<p>If <code class="language-plaintext highlighter-rouge">set</code> fails with security context errors:</p>

<ul>
  <li>Run PowerShell as Administrator.</li>
</ul>

<p>If the agent is not installed:</p>

<ul>
  <li>Install Azure Arc Connected Machine Agent (<code class="language-plaintext highlighter-rouge">azcmagent</code>) first.</li>
</ul>

<hr />

<h2 id="summary">Summary</h2>

<p>This catalog gives you a practical DSC v3 interface for Azure Arc agent local configuration. The best operational pattern is:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">export</code> current state as baseline.</li>
  <li>Define desired hardened settings.</li>
  <li><code class="language-plaintext highlighter-rouge">set</code> and then <code class="language-plaintext highlighter-rouge">test</code> for drift-free compliance.</li>
</ol>

<p>It is simple, scriptable, and easy to plug into CI/CD pipelines for consistent Arc agent posture across servers.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://github.com/PowerShell/DSC">Microsoft DSCv3 Github</a></li>
  <li><a href="https://github.com/mimachniak/AzureConnectedMachineDscV3">DSC Resources Github</a></li>
  <li><a href="https://github.com/mimachniak/sysopslife-scripts">Blog code examples Github</a></li>
</ul>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="DSC" /><category term="Azure" /><category term="DSCv3" /><category term="AzureArc" /><category term="IaC" /><category term="PowerShell" /><summary type="html"><![CDATA[How to use DSC v3 resources for Azure Arc, what configuration options can be set, and practical export and set examples for Microsoft.Azure.Arc/AgentConfiguration.]]></summary></entry><entry><title type="html">Auto-Documenting DSC v3 Drift and Compliance with DSC-DocsGenerator (CIS Example)</title><link href="https://mimachniak.github.io/sysopslife/2026/06/16/dscv3-auto-document-report-with-cis-example/" rel="alternate" type="text/html" title="Auto-Documenting DSC v3 Drift and Compliance with DSC-DocsGenerator (CIS Example)" /><published>2026-06-16T00:00:00+02:00</published><updated>2026-06-16T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2026/06/16/dscv3-auto-document-report-with-cis-example</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2026/06/16/dscv3-auto-document-report-with-cis-example/"><![CDATA[<p><img src="/assets/images/DSC/dsc-flow.PNG" alt="" /></p>

<h1 id="auto-documenting-dsc-v3-drift-and-compliance-with-dsc-docsgenerator">Auto-Documenting DSC v3 Drift and Compliance with DSC-DocsGenerator</h1>

<h2 id="introduction">Introduction</h2>

<p>DSC v3 is excellent at telling you whether a target is in the desired state — but the raw output of <code class="language-plaintext highlighter-rouge">dsc config test</code> is JSON intended for tooling, not humans. Auditors, security reviewers, and operations teams need something they can read, share, and attach to a change record: a report that says <em>what was configured</em>, <em>what was actually found on the machine</em>, and <em>which controls drifted</em>.</p>

<p>That is the gap <a href="https://github.com/mimachniak/DSC-DocsGenerator">DSC-DocsGenerator</a> fills. It is a small PowerShell module that wraps <code class="language-plaintext highlighter-rouge">dsc config test</code>, parses the JSON output, and emits a Markdown report containing:</p>

<ul>
  <li>A configuration summary (OS, hostname, file, run time, document version).</li>
  <li>A resource overview table with per-control compliance status.</li>
  <li>A detailed desired vs actual state block for every resource.</li>
</ul>

<p>The module is published to the PowerShell Gallery as <a href="https://www.powershellgallery.com/packages/DSC-DocsGenerator">DSC-DocsGenerator</a> and the source lives at <a href="https://github.com/mimachniak/DSC-DocsGenerator">mimachniak/DSC-DocsGenerator</a>.</p>

<p>This post walks through what the module does, how to install and run it, and shows a real CIS Windows Server 2025 example with drift and compliance points.</p>

<hr />

<h2 id="why-an-auto-generated-compliance-report">Why an Auto-Generated Compliance Report?</h2>

<p>DSC v3’s <code class="language-plaintext highlighter-rouge">test</code> subcommand returns structured JSON describing, for each resource, whether the system is <code class="language-plaintext highlighter-rouge">_inDesiredState</code>, which properties differ, the <strong>desired</strong> state from the YAML, and the <strong>actual</strong> state collected from the machine. That JSON is precise but hard to consume.</p>

<p>A Markdown report on top of that JSON gives several practical wins:</p>

<table>
  <thead>
    <tr>
      <th>Need</th>
      <th>What the report provides</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Audit evidence</td>
      <td>Timestamped file with hostname, OS build, and source YAML name</td>
    </tr>
    <tr>
      <td>Drift review</td>
      <td>Per-control ✅ / ❌ status table with differing property names</td>
    </tr>
    <tr>
      <td>Remediation triage</td>
      <td>Side-by-side desired vs actual JSON for each non-compliant control</td>
    </tr>
    <tr>
      <td>Change documentation</td>
      <td>Single Markdown artifact that can be committed, attached to a ticket, or pasted into a wiki</td>
    </tr>
  </tbody>
</table>

<p>The same flow works for any DSC v3 configuration — application baselines, WinRM hardening, ActiveDirectory roles — but a CIS benchmark is the clearest demonstration because it produces a large number of controls with a realistic mix of compliant and non-compliant results.</p>

<hr />

<h2 id="how-the-module-works">How the Module Works</h2>

<p>The single public command is <code class="language-plaintext highlighter-rouge">Invoke-DSCDrifftDocs</code> (note the intentional double <code class="language-plaintext highlighter-rouge">f</code> in <code class="language-plaintext highlighter-rouge">Drifft</code> — it must be typed exactly).</p>

<p>When invoked, it performs these steps internally:</p>

<ol>
  <li>Verifies the <code class="language-plaintext highlighter-rouge">dsc</code> CLI is installed and on <code class="language-plaintext highlighter-rouge">PATH</code>.</li>
  <li>Reads the YAML config and checks that the referenced resource types are available.</li>
  <li>Runs <code class="language-plaintext highlighter-rouge">dsc config test --file &lt;ConfigFile&gt;</code>.</li>
  <li>Parses the JSON returned by DSC.</li>
  <li>Builds the Markdown report (summary, overview table, per-resource details).</li>
  <li>Writes the report file unless <code class="language-plaintext highlighter-rouge">-WhatIf</code> is used.</li>
</ol>

<p>That is the whole pipeline: <strong>YAML in → DSC test → JSON → Markdown out</strong>.</p>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li>PowerShell 5.1 or PowerShell 7+.</li>
  <li>
    <p>DSC v3 installed and available on <code class="language-plaintext highlighter-rouge">PATH</code> (<code class="language-plaintext highlighter-rouge">dsc --version</code> works). Install with:</p>

    <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nt">--id</span><span class="w"> </span><span class="nx">Microsoft.DSC</span><span class="w">
</span></code></pre></div>    </div>
  </li>
  <li>The DSC resources referenced by the YAML config installed. For the CIS examples that is <code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code> (built-in to DSC v3) and <code class="language-plaintext highlighter-rouge">SecurityPolicyDsc</code>.</li>
</ul>

<hr />

<h2 id="install-the-module">Install the Module</h2>

<p>Three options, in order of convenience:</p>

<h3 id="option-1--powershell-gallery-recommended">Option 1 — PowerShell Gallery (recommended)</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">DSC-DocsGenerator</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">DSC-DocsGenerator</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<h3 id="option-2--clone-and-import-from-the-repo">Option 2 — Clone and import from the repo</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">git</span><span class="w"> </span><span class="nx">clone</span><span class="w"> </span><span class="nx">https://github.com/mimachniak/DSC-DocsGenerator.git</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="o">.</span><span class="nx">\DSC-DocsGenerator\module\DSC-DocsGenerator\DSC-DocsGenerator.psd1</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<h3 id="option-3--copy-to-envpsmodulepath">Option 3 — Copy to <code class="language-plaintext highlighter-rouge">$env:PSModulePath</code></h3>

<p>Copy the <code class="language-plaintext highlighter-rouge">module\DSC-DocsGenerator</code> folder to any path in <code class="language-plaintext highlighter-rouge">$env:PSModulePath</code>, then:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Module</span><span class="w"> </span><span class="nx">DSC-DocsGenerator</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<p>Verify the command is exported:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Command</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="nx">DSC-DocsGenerator</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="command-syntax">Command Syntax</h2>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-DSCDrifftDocs</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ConfigFile</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">string</span><span class="err">&gt;</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="p">[</span><span class="nt">-OutputFile</span><span class="w"> </span><span class="err">&lt;</span><span class="n">string</span><span class="err">&gt;</span><span class="p">]</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="p">[</span><span class="nt">-DocumentVersion</span><span class="w"> </span><span class="err">&lt;</span><span class="n">string</span><span class="err">&gt;</span><span class="p">]</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="p">[</span><span class="nt">-ReportTitle</span><span class="w"> </span><span class="err">&lt;</span><span class="n">string</span><span class="err">&gt;</span><span class="p">]</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="p">[</span><span class="nt">-PassThru</span><span class="p">]</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="p">[</span><span class="nt">-WhatIf</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="nt">-Confirm</span><span class="p">]</span><span class="w"> </span><span class="p">[</span><span class="nt">-Verbose</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Required</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ConfigFile</code></td>
      <td>Yes</td>
      <td>Path to a DSC v3 YAML configuration file.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OutputFile</code></td>
      <td>No</td>
      <td>Target Markdown file path. If omitted, a timestamped file is generated next to the config.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DocumentVersion</code></td>
      <td>No</td>
      <td>Version label written into the report. Default: <code class="language-plaintext highlighter-rouge">1.0</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ReportTitle</code></td>
      <td>No</td>
      <td>Report heading (H1). Default: <code class="language-plaintext highlighter-rouge">DSC Configuration Report</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PassThru</code></td>
      <td>No</td>
      <td>Returns the generated Markdown content as a string.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">WhatIf</code></td>
      <td>No</td>
      <td>Preview actions without writing files.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="example--cis-windows-server-2025-level-1-member-server">Example — CIS Windows Server 2025 Level 1 Member Server</h2>

<p>The repository ships with two ready-to-run CIS baselines under <code class="language-plaintext highlighter-rouge">dsc-config\</code>:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">CIS-w2025-Level1-MemberServer.registry.dsc.yaml</code> — 260 registry controls.</li>
  <li><code class="language-plaintext highlighter-rouge">CIS-w2025-Level1-MemberServer.securitypolicy.dsc.yaml</code> — 38 account policy, user rights, and security option controls.</li>
</ul>

<h3 id="1-generate-the-registry-compliance-report">1) Generate the registry compliance report</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-DSCDrifftDocs</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ConfigFile</span><span class="w"> </span><span class="o">.</span><span class="nx">\dsc-config\CIS-w2025-Level1-MemberServer.registry.dsc.yaml</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-OutputFile</span><span class="w"> </span><span class="o">.</span><span class="nx">\report-example\registry-report.md</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ReportTitle</span><span class="w"> </span><span class="s2">"CIS Windows Server 2025 - Registry Compliance"</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-DocumentVersion</span><span class="w"> </span><span class="s2">"2.0"</span><span class="w">
</span></code></pre></div></div>

<h3 id="2-generate-the-security-policy-report-with-default-output">2) Generate the security policy report with default output</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-DSCDrifftDocs</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ConfigFile</span><span class="w"> </span><span class="o">.</span><span class="nx">\dsc-config\CIS-w2025-Level1-MemberServer.securitypolicy.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<h3 id="3-capture-the-markdown-in-memory-no-file-written-next-to-the-yaml">3) Capture the Markdown in memory (no file written next to the YAML)</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$markdown</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-DSCDrifftDocs</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ConfigFile</span><span class="w"> </span><span class="o">.</span><span class="nx">\dsc-config\CIS-w2025-Level1-MemberServer.registry.dsc.yaml</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-PassThru</span><span class="w">

</span><span class="nv">$markdown</span><span class="o">.</span><span class="nf">Length</span><span class="w">
</span></code></pre></div></div>

<h3 id="4-preview-without-writing">4) Preview without writing</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-DSCDrifftDocs</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ConfigFile</span><span class="w"> </span><span class="o">.</span><span class="nx">\dsc-config\CIS-w2025-Level1-MemberServer.registry.dsc.yaml</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-WhatIf</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="what-the-generated-report-looks-like">What the Generated Report Looks Like</h2>

<p>The full rendered examples live in <a href="https://github.com/mimachniak/DSC-DocsGenerator/tree/main/report-example">report-example/</a>. Below are the key sections, taken from the registry baseline run on a freshly built Windows Server 2025 box.</p>

<h3 id="configuration-summary">Configuration Summary</h3>

<p>The report opens with a fixed metadata block — exactly the information an auditor wants on the first page:</p>

<table>
  <thead>
    <tr>
      <th>Property</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>OS</strong></td>
      <td>Microsoft Windows Server 2025 Datacenter (Build 26100, 64-bit)</td>
    </tr>
    <tr>
      <td><strong>Hostname</strong></td>
      <td>WIN-3QDVF8G0A7K</td>
    </tr>
    <tr>
      <td><strong>Configuration File</strong></td>
      <td>CIS-w2025-Level1-MemberServer.registry.dsc.yaml</td>
    </tr>
    <tr>
      <td><strong>Run Date / Time</strong></td>
      <td>2026-06-01 13:21:59</td>
    </tr>
    <tr>
      <td><strong>Document Version</strong></td>
      <td>1.0</td>
    </tr>
    <tr>
      <td><strong>Total Resources</strong></td>
      <td>260</td>
    </tr>
    <tr>
      <td><strong>Compliant</strong></td>
      <td>✅ 32</td>
    </tr>
    <tr>
      <td><strong>Non-Compliant</strong></td>
      <td>❌ 228</td>
    </tr>
  </tbody>
</table>

<p>On a default-installed Server 2025 image the registry baseline is 32/260 compliant — exactly what you would expect before any CIS hardening is applied. That ratio becomes the headline KPI you track across remediation runs.</p>

<h3 id="resource-overview">Resource Overview</h3>

<p>Below the summary the report lists every control with its CIS reference, status, and DSC type:</p>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Status</th>
      <th>Name</th>
      <th>Type</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>❌</td>
      <td><code class="language-plaintext highlighter-rouge">1.1.6 - RelaxMinimumPasswordLengthLimits</code></td>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code></td>
    </tr>
    <tr>
      <td>2</td>
      <td>✅</td>
      <td><code class="language-plaintext highlighter-rouge">2.3.1.2 - LimitBlankPasswordUse</code></td>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code></td>
    </tr>
    <tr>
      <td>3</td>
      <td>❌</td>
      <td><code class="language-plaintext highlighter-rouge">2.3.2.1 - SCENoApplyLegacyAuditPolicy</code></td>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code></td>
    </tr>
    <tr>
      <td>4</td>
      <td>✅</td>
      <td><code class="language-plaintext highlighter-rouge">2.3.2.2 - CrashOnAuditFail</code></td>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code></td>
    </tr>
    <tr>
      <td>…</td>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>57</td>
      <td>❌</td>
      <td><code class="language-plaintext highlighter-rouge">9.1.1 - EnableFirewall</code></td>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code></td>
    </tr>
    <tr>
      <td>58</td>
      <td>❌</td>
      <td><code class="language-plaintext highlighter-rouge">9.1.2 - DefaultInboundAction</code></td>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code></td>
    </tr>
  </tbody>
</table>

<p>This table is the triage view: filter to ❌ rows and you have a backlog of CIS controls to remediate, each with its benchmark number for traceability.</p>

<h3 id="per-resource-drift-detail">Per-Resource Drift Detail</h3>

<p>For every control the report renders a block with the differing properties and the full desired vs actual JSON. A compliant control:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">### 4. 2.3.2.2 - CrashOnAuditFail</span>

| Property | Value |
|----------|-------|
| <span class="gs">**Status**</span> | ✅ Compliant |
| <span class="gs">**Type**</span> | <span class="sb">`Microsoft.Windows/Registry`</span> |

<span class="gs">**Desired State**</span>

<span class="p">```</span><span class="nl">json
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"_exist"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"keyPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HKEY_LOCAL_MACHINE</span><span class="se">\\</span><span class="s2">SYSTEM</span><span class="se">\\</span><span class="s2">CurrentControlSet</span><span class="se">\\</span><span class="s2">Control</span><span class="se">\\</span><span class="s2">Lsa"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CrashOnAuditFail"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueData"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"DWord"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span>
<span class="p">```</span>

<span class="gs">**Actual State**</span>

<span class="p">```</span><span class="nl">json
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"keyPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HKEY_LOCAL_MACHINE</span><span class="se">\\</span><span class="s2">SYSTEM</span><span class="se">\\</span><span class="s2">CurrentControlSet</span><span class="se">\\</span><span class="s2">Control</span><span class="se">\\</span><span class="s2">Lsa"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CrashOnAuditFail"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueData"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"DWord"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span>
<span class="p">```</span>
</code></pre></div></div>

<p>A non-compliant control with a value mismatch — CIS 2.3.7.2 <em>Interactive logon: Do not display last user name</em>:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">### 12. 2.3.7.2 - DontDisplayLastUserName</span>

| Property | Value |
|----------|-------|
| <span class="gs">**Status**</span> | ❌ Non-Compliant |
| <span class="gs">**Type**</span> | <span class="sb">`Microsoft.Windows/Registry`</span> |
| <span class="gs">**Differing Properties**</span> | <span class="sb">`valueData`</span> |

<span class="gs">**Desired State**</span>

<span class="p">```</span><span class="nl">json
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"_exist"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"keyPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HKEY_LOCAL_MACHINE</span><span class="se">\\</span><span class="s2">SOFTWARE</span><span class="se">\\</span><span class="s2">Microsoft</span><span class="se">\\</span><span class="s2">Windows</span><span class="se">\\</span><span class="s2">CurrentVersion</span><span class="se">\\</span><span class="s2">Policies</span><span class="se">\\</span><span class="s2">System"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DontDisplayLastUserName"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueData"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"DWord"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span>
<span class="p">```</span>

<span class="gs">**Actual State**</span>

<span class="p">```</span><span class="nl">json
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"keyPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HKEY_LOCAL_MACHINE</span><span class="se">\\</span><span class="s2">SOFTWARE</span><span class="se">\\</span><span class="s2">Microsoft</span><span class="se">\\</span><span class="s2">Windows</span><span class="se">\\</span><span class="s2">CurrentVersion</span><span class="se">\\</span><span class="s2">Policies</span><span class="se">\\</span><span class="s2">System"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DontDisplayLastUserName"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueData"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"DWord"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span>
<span class="p">```</span>
</code></pre></div></div>

<p>And a missing-key drift — CIS 9.1.1 <em>Windows Firewall: Domain: Firewall state</em> not configured at all:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">### 57. 9.1.1 - EnableFirewall</span>

| Property | Value |
|----------|-------|
| <span class="gs">**Status**</span> | ❌ Non-Compliant |
| <span class="gs">**Type**</span> | <span class="sb">`Microsoft.Windows/Registry`</span> |
| <span class="gs">**Differing Properties**</span> | <span class="sb">`_exist`</span>, <span class="sb">`valueName`</span>, <span class="sb">`valueData`</span> |

<span class="gs">**Desired State**</span>

<span class="p">```</span><span class="nl">json
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"_exist"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"keyPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HKEY_LOCAL_MACHINE</span><span class="se">\\</span><span class="s2">SOFTWARE</span><span class="se">\\</span><span class="s2">Policies</span><span class="se">\\</span><span class="s2">Microsoft</span><span class="se">\\</span><span class="s2">WindowsFirewall</span><span class="se">\\</span><span class="s2">DomainProfile"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"EnableFirewall"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"valueData"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"DWord"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span>
<span class="p">```</span>

<span class="gs">**Actual State**</span>

<span class="p">```</span><span class="nl">json
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"keyPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HKEY_LOCAL_MACHINE</span><span class="se">\\</span><span class="s2">SOFTWARE</span><span class="se">\\</span><span class="s2">Policies</span><span class="se">\\</span><span class="s2">Microsoft</span><span class="se">\\</span><span class="s2">WindowsFirewall</span><span class="se">\\</span><span class="s2">DomainProfile"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"_exist"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span>
<span class="p">```</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Differing Properties</code> row is the most useful piece of metadata: it points straight to which fields drifted (<code class="language-plaintext highlighter-rouge">valueData</code> only, vs. the whole key being absent), so you can decide whether the gap is a missed GPO, a wrong value, or a key never created.</p>

<hr />

<h2 id="the-security-policy-baseline">The Security Policy Baseline</h2>

<p>The second baseline — <code class="language-plaintext highlighter-rouge">SecurityPolicyDsc/AccountPolicy</code>, <code class="language-plaintext highlighter-rouge">SecurityPolicyDsc/UserRightsAssignment</code>, <code class="language-plaintext highlighter-rouge">SecurityPolicyDsc/SecurityOption</code> — produces the same shape of report but exercises a different resource provider. On the same fresh Server 2025 the summary is 26/38 compliant, and the drift blocks include rich examples like <em>2.2.21 Deny access to this computer from the network</em>:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">### 16. 2.2.21 Deny access to this computer from the network</span>

| Property | Value |
|----------|-------|
| <span class="gs">**Status**</span> | ❌ Non-Compliant |
| <span class="gs">**Type**</span> | <span class="sb">`SecurityPolicyDsc/UserRightsAssignment`</span> |
| <span class="gs">**Differing Properties**</span> | <span class="sb">`Identity`</span>, <span class="sb">`Force`</span> |

<span class="gs">**Desired State**</span>

<span class="p">```</span><span class="nl">json
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"Policy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Deny_access_to_this_computer_from_the_network"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Identity"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"Guests"</span><span class="p">,</span><span class="w"> </span><span class="s2">"[Local Account|Administrator]"</span><span class="w"> </span><span class="p">],</span><span class="w">
  </span><span class="nl">"Force"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span>
<span class="p">```</span>
</code></pre></div></div>

<p>The same module, the same command — only the input YAML changes.</p>

<hr />

<h2 id="where-this-fits-in-a-dsc-v3-workflow">Where This Fits in a DSC v3 Workflow</h2>

<p>This module is the <em>reporting</em> leg of the lifecycle described in the earlier posts in this series:</p>

<ul>
  <li><a href="/2026/05/13/dscv3-and-powershell-setup-winrm/">Setting Up WinRM HTTPS Listener with DSC v3 and PowerShell</a> — authoring DSC v3 YAML configurations.</li>
  <li><a href="/2026/05/21/bicep-and-dscv3-deploy-and-configure/">Deploy and Configure a Windows VM with Bicep and DSC v3</a> — applying configurations as part of an IaC deployment.</li>
  <li><a href="/2026/02/09/hot-to-use-dscv3-in-azure-devops/">How to use DSC v3 in Azure DevOps</a> — pipelining the apply step.</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">Invoke-DSCDrifftDocs</code> plugs into the same pipelines: run it in a scheduled job or release stage after <code class="language-plaintext highlighter-rouge">dsc config test</code>, publish the resulting Markdown as a pipeline artifact, and you have a per-host audit trail without writing a single line of custom JSON parsing.</p>

<hr />

<h2 id="troubleshooting">Troubleshooting</h2>

<p><strong><code class="language-plaintext highlighter-rouge">dsc</code> not found</strong> — Install DSC v3 with <code class="language-plaintext highlighter-rouge">winget install --id Microsoft.DSC</code> and confirm with <code class="language-plaintext highlighter-rouge">dsc --version</code>.</p>

<p><strong>Missing DSC resources</strong> — Install the modules referenced by the YAML (for the CIS examples that is <code class="language-plaintext highlighter-rouge">SecurityPolicyDsc</code>) and rerun.</p>

<p><strong>No report written</strong> — Check whether <code class="language-plaintext highlighter-rouge">-WhatIf</code> was used, and confirm the output folder is writable.</p>

<p><strong>Run with diagnostics</strong> — Add <code class="language-plaintext highlighter-rouge">-Verbose</code> to see each pipeline step.</p>

<hr />

<h2 id="wrap-up">Wrap-Up</h2>

<p>DSC-DocsGenerator turns the raw JSON output of <code class="language-plaintext highlighter-rouge">dsc config test</code> into a Markdown compliance report with summary metrics, a per-control overview, and full desired vs actual state for every resource — in a single command. Run it once against the bundled CIS Windows Server 2025 baselines and you immediately have an audit-quality document showing exactly where the box drifts from the benchmark.</p>

<ul>
  <li>Module: <a href="https://github.com/mimachniak/DSC-DocsGenerator">github.com/mimachniak/DSC-DocsGenerator</a></li>
  <li>PowerShell Gallery: <a href="https://www.powershellgallery.com/packages/DSC-DocsGenerator">DSC-DocsGenerator</a></li>
  <li>Example reports: <a href="https://github.com/mimachniak/DSC-DocsGenerator/tree/main/report-example">report-example/</a></li>
</ul>

<p>Pull requests and issues are welcome.</p>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="DSC" /><category term="PowerShell" /><category term="IaaC" /><category term="DSCv3" /><category term="CIS" /><category term="Compliance" /><category term="Reporting" /><summary type="html"><![CDATA[Introducing DSC-DocsGenerator — a PowerShell module that runs `dsc config test` against a DSC v3 YAML configuration, parses the JSON output, and produces a complete Markdown compliance report. Demonstrated end-to-end with a CIS Windows Server 2025 Level 1 Member Server baseline showing drift, desired vs actual state, and a per-resource compliance breakdown.]]></summary></entry><entry><title type="html">Deploy and Configure a Windows VM with Bicep and DSC v3</title><link href="https://mimachniak.github.io/sysopslife/2026/05/21/bicep-and-dscv3-deploy-and-configure/" rel="alternate" type="text/html" title="Deploy and Configure a Windows VM with Bicep and DSC v3" /><published>2026-05-21T00:00:00+02:00</published><updated>2026-05-21T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2026/05/21/bicep-and-dscv3-deploy-and-configure</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2026/05/21/bicep-and-dscv3-deploy-and-configure/"><![CDATA[<p><img src="/assets/images/DSC/dsc-flow.PNG" alt="" /></p>

<h1 id="deploy-and-configure-a-windows-vm-with-bicep-and-dsc-v3">Deploy and Configure a Windows VM with Bicep and DSC v3</h1>

<h2 id="introduction">Introduction</h2>

<p>Infrastructure as Code (IaC) covers not only resource provisioning but also the initial operating system configuration. Bicep handles the Azure resources; DSC v3 handles the Windows configuration. Combining them in a single deployment means the VM is provisioned <strong>and</strong> correctly configured before the deployment finishes.</p>

<p>This post walks through deploying a Windows Server 2025 Azure VM with Bicep and then using two DSC v3 YAML documents — served from Azure Blob Storage — to:</p>

<ol>
  <li>Create a self-signed certificate bound to the computer name.</li>
  <li>Configure a secure <strong>WinRM HTTPS listener</strong> on port 5986.</li>
</ol>

<p>The DSC YAML files are covered in detail in the companion post <a href="/2026/05/13/dscv3-and-powershell-setup-winrm/">Setting Up WinRM HTTPS Listener with DSC v3 and PowerShell</a>.</p>

<hr />

<h2 id="architecture-overview">Architecture Overview</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────────────────────────────────────────────┐
│  Bicep deployment (New-AzResourceGroupDeployment)    │
│                                                      │
│  1. Storage Account + container (dsc)                │
│  2. VNet / NSG / NIC / Public IP                     │
│  3. Windows Server 2025 VM                           │
│  4. Run Command: install DSC v3                      │
│  5. Run Command: apply DSC configs from blob         │
└──────────────────────────────────────────────────────┘
          │                        │
          ▼                        ▼
   Azure Blob Storage       VM (after deploy)
   dsc/                     ├─ DSC v3 installed
   ├─ ps-script-            ├─ Self-signed cert
   │  certificate.          │  in LocalMachine\My
   │  dsc.yaml              └─ WinRM HTTPS :5986
   └─ winrm.dsc.yaml
</code></pre></div></div>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li>Azure CLI or Azure PowerShell installed and authenticated.</li>
  <li>An Azure subscription and a resource group.</li>
  <li>The DSC YAML files locally (or cloned from the companion repository).</li>
</ul>

<hr />

<h2 id="step-1--prepare-the-dsc-yaml-documents">Step 1 — Prepare the DSC YAML Documents</h2>

<p>The configuration is split into two focused DSC v3 YAML documents. Place both files in a local folder before uploading them.</p>

<h3 id="ps-script-certificatedscyaml"><code class="language-plaintext highlighter-rouge">ps-script-certificate.dsc.yaml</code></h3>

<p>Uses the <code class="language-plaintext highlighter-rouge">Microsoft.DSC.Transitional/PowerShellScript</code> resource to generate a self-signed certificate in <code class="language-plaintext highlighter-rouge">Cert:\LocalMachine\My</code> if one does not already exist for the current computer name.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>

<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span>
<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC.Transitional/PowerShellScript</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">CreateSelfSignedCertificate</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">GetScript</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">$dnsName = $env:COMPUTERNAME</span>
          <span class="s">$cert = Get-ChildItem Cert:\LocalMachine\My |</span>
              <span class="s">Where-Object {</span>
                  <span class="s">$_.Subject -eq "CN=$dnsName"</span>
              <span class="s">}</span>

          <span class="s">return @{</span>
              <span class="s">Result = if ($cert) { "Present" } else { "Absent" }</span>
              <span class="s">Thumbprint = if ($cert) { $cert.Thumbprint } else { $null }</span>
          <span class="s">}</span>
      <span class="na">TestScript</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">$dnsName = $env:COMPUTERNAME</span>
          <span class="s">$cert = Get-ChildItem Cert:\LocalMachine\My |</span>
              <span class="s">Where-Object {</span>
                  <span class="s">$_.Subject -eq "CN=$dnsName"</span>
              <span class="s">}</span>

          <span class="s">return ($null -ne $cert)</span>
      <span class="na">SetScript</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">$dnsName = $env:COMPUTERNAME</span>
        <span class="s">$cert = Get-ChildItem Cert:\LocalMachine\My |</span>
              <span class="s">Where-Object {</span>
                  <span class="s">$_.Subject -eq "CN=$dnsName"</span>
              <span class="s">}</span>
        <span class="s">if (-not $cert) {</span>
            <span class="s">$cert = New-SelfSignedCertificate -DnsName $dnsName -CertStoreLocation "Cert:\LocalMachine\My" -KeyLength 2048 -HashAlgorithm SHA256</span>
            <span class="s">}</span>
        <span class="s">return @{ Thumbprint = $cert.Thumbprint }</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">SetScript</code> returns the certificate thumbprint in the <code class="language-plaintext highlighter-rouge">afterState</code> output. The Bicep run command extracts it and forwards it as a parameter to the next document.</p>

<hr />

<h3 id="winrmdscyaml"><code class="language-plaintext highlighter-rouge">winrm.dsc.yaml</code></h3>

<p>Uses native DSC v3 resources (<code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code>, <code class="language-plaintext highlighter-rouge">Microsoft.Windows/FirewallRuleList</code>, <code class="language-plaintext highlighter-rouge">Microsoft.Windows/Service</code>) to write the WSMAN listener registry keys, open port 5986, and ensure the WinRM service is running.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>

<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">restricted</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">certThumbprint</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s">Thumbprint of the certificate to use for the WinRM HTTPS listener.</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">341B58BC25D182D844B64BF7FC52D1D751625960"</span>

<span class="na">resources</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - Address</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
    <span class="na">valueName</span><span class="pi">:</span> <span class="s">Address</span>
    <span class="na">valueData</span><span class="pi">:</span>
      <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - Transport</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
    <span class="na">valueName</span><span class="pi">:</span> <span class="s">Transport</span>
    <span class="na">valueData</span><span class="pi">:</span>
      <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HTTPS"</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - Port</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
    <span class="na">valueName</span><span class="pi">:</span> <span class="s">Port</span>
    <span class="na">valueData</span><span class="pi">:</span>
      <span class="na">DWord</span><span class="pi">:</span> <span class="m">5986</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - certThumbprint</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
    <span class="na">valueName</span><span class="pi">:</span> <span class="s">certThumbprint</span>
    <span class="na">valueData</span><span class="pi">:</span>
      <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('certThumbprint')]"</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - hostname</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
    <span class="na">valueName</span><span class="pi">:</span> <span class="s">hostname</span>
    <span class="na">valueData</span><span class="pi">:</span>
      <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[envvar('COMPUTERNAME')]"</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM firewall rules</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/FirewallRuleList</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">rules</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Windows Remote Management (HTTPS-In)</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s">Allow inbound TCP traffic on port 5986 for Windows Remote Management (HTTPS-In).</span>
        <span class="na">applicationName</span><span class="pi">:</span> <span class="s">System</span>
        <span class="na">protocol</span><span class="pi">:</span> <span class="m">6</span>
        <span class="na">localPorts</span><span class="pi">:</span> <span class="s1">'</span><span class="s">5986'</span>
        <span class="na">direction</span><span class="pi">:</span> <span class="s">Inbound</span>
        <span class="na">action</span><span class="pi">:</span> <span class="s">Allow</span>
        <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">profiles</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">Domain</span>
          <span class="pi">-</span> <span class="s">Private</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM Service</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Service</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM</span>
    <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">status</span><span class="pi">:</span> <span class="s">Running</span>
    <span class="na">startType</span><span class="pi">:</span> <span class="s">Automatic</span>
</code></pre></div></div>

<hr />

<h2 id="step-2--create-the-azure-storage-account-and-blob-container">Step 2 — Create the Azure Storage Account and Blob Container</h2>

<p>The Bicep template expects the YAML files to be reachable via an HTTPS URL (anonymous read or SAS token). Create a dedicated storage account and container before running the deployment.</p>

<h3 id="using-azure-powershell">Using Azure PowerShell</h3>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$rg</span><span class="w">            </span><span class="o">=</span><span class="w"> </span><span class="s1">'demo-dsc-bicep-rg'</span><span class="w">
</span><span class="nv">$location</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="s1">'West Europe'</span><span class="w">
</span><span class="nv">$storageName</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s1">'dscconfigstore'</span><span class="w">   </span><span class="c"># must be globally unique</span><span class="w">
</span><span class="nv">$containerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'dsc'</span><span class="w">

</span><span class="c"># Create resource group</span><span class="w">
</span><span class="n">New-AzResourceGroup</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$rg</span><span class="w"> </span><span class="nt">-Location</span><span class="w"> </span><span class="nv">$location</span><span class="w">

</span><span class="c"># Create storage account (Standard LRS, public blob read enabled)</span><span class="w">
</span><span class="nv">$sa</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-AzStorageAccount</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ResourceGroupName</span><span class="w"> </span><span class="nv">$rg</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$storageName</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Location</span><span class="w"> </span><span class="nv">$location</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-SkuName</span><span class="w"> </span><span class="nx">Standard_LRS</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Kind</span><span class="w"> </span><span class="nx">StorageV2</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-AllowBlobPublicAccess</span><span class="w"> </span><span class="bp">$true</span><span class="w">

</span><span class="c"># Create container with anonymous blob-level read access</span><span class="w">
</span><span class="nv">$ctx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$sa</span><span class="o">.</span><span class="nf">Context</span><span class="w">
</span><span class="n">New-AzStorageContainer</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$containerName</span><span class="w"> </span><span class="nt">-Context</span><span class="w"> </span><span class="nv">$ctx</span><span class="w"> </span><span class="nt">-Permission</span><span class="w"> </span><span class="nx">Blob</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>Security note:</strong> Anonymous public blob access is convenient for demos. For production deployments, disable public access and supply a short-lived SAS token to the Bicep parameters instead.</p>
</blockquote>

<hr />

<h2 id="step-3--upload-the-dsc-yaml-files">Step 3 — Upload the DSC YAML Files</h2>

<blockquote>
  <p><strong>Yaml files</strong> can be found here: <a href="https://github.com/mimachniak/sysopslife-scripts/tree/master/DSC/V3/winrm">DSC Yaml configurations</a></p>
</blockquote>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ctx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-AzStorageAccount</span><span class="w"> </span><span class="nt">-ResourceGroupName</span><span class="w"> </span><span class="nv">$rg</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$storageName</span><span class="p">)</span><span class="o">.</span><span class="nf">Context</span><span class="w">

</span><span class="n">Set-AzStorageBlobContent</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-File</span><span class="w">   </span><span class="s1">'.\ps-script-certificate.dsc.yaml'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Container</span><span class="w"> </span><span class="nv">$containerName</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Blob</span><span class="w">   </span><span class="s1">'ps-script-certificate.dsc.yaml'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Context</span><span class="w"> </span><span class="nv">$ctx</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Force</span><span class="w">

</span><span class="n">Set-AzStorageBlobContent</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-File</span><span class="w">   </span><span class="s1">'.\winrm.dsc.yaml'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Container</span><span class="w"> </span><span class="nv">$containerName</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Blob</span><span class="w">   </span><span class="s1">'winrm.dsc.yaml'</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Context</span><span class="w"> </span><span class="nv">$ctx</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<p>Note the blob base URL — you will need it in the next step:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://&lt;your-storage-account&gt;.blob.core.windows.net/dsc/
</code></pre></div></div>

<hr />

<h2 id="step-4--review-and-update-mainbicep">Step 4 — Review and Update <code class="language-plaintext highlighter-rouge">main.bicep</code></h2>

<p>The full Bicep template provisions the network stack, the VM, and two <code class="language-plaintext highlighter-rouge">Microsoft.Compute/virtualMachines/runCommands</code> resources that install DSC v3 and apply the configuration.</p>

<h3 id="update-the-blob-urls">Update the blob URLs</h3>

<p>Locate the <code class="language-plaintext highlighter-rouge">runCommandsApplyDSC</code> resource in <a href="https://github.com/mimachniak/sysopslife-scripts/blob/master/DSC/V3/bicep-demo-dsc/main.bicep">main.bicep</a> and replace the placeholder URLs with your actual storage account blob URLs:</p>

<p><strong>Bicep code main:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Replace with your storage account url</span><span class="w">

</span><span class="nv">$contentYamlCert</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"https://&lt;your-storage-account&gt;.blob.core.windows.net/dsc/ps-script-certificate.dsc.yaml"</span><span class="w">

</span><span class="c"># Replace with your storage account url</span><span class="w">

</span><span class="nv">$contentYamlWinrm</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="s2">"https://&lt;your-storage-account&gt;.blob.core.windows.net/dsc/winrm.dsc.yaml"</span><span class="w">

</span></code></pre></div></div>

<hr />

<h2 id="step-5--deploy-with-bicep">Step 5 — Deploy with Bicep</h2>

<blockquote>
  <p><strong>Bicep main file</strong> for this example with all steps can be found here: <a href="https://github.com/mimachniak/sysopslife-scripts/tree/master/DSC/V3/bicep-demo-dsc">bicep-demo-dsc </a></p>
</blockquote>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$exampleRG</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'demo-dsc-bicep-rg'</span><span class="w">
</span><span class="nv">$adminUser</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s1">'godmode'</span><span class="w">
</span><span class="nv">$location</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="s1">'West Europe'</span><span class="w">

</span><span class="n">New-AzResourceGroup</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$exampleRG</span><span class="w"> </span><span class="nt">-Location</span><span class="w"> </span><span class="nv">$location</span><span class="w">

</span><span class="n">New-AzResourceGroupDeployment</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-ResourceGroupName</span><span class="w"> </span><span class="nv">$exampleRG</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-TemplateFile</span><span class="w">      </span><span class="o">.</span><span class="nx">/main.bicep</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-adminUsername</span><span class="w">     </span><span class="nv">$adminUser</span><span class="w"> </span><span class="se">`
</span><span class="w">    </span><span class="nt">-Verbose</span><span class="w">
</span></code></pre></div></div>

<p>You will be prompted for <code class="language-plaintext highlighter-rouge">adminPassword</code> (minimum 12 characters).</p>

<hr />

<h2 id="how-it-works--run-command-walk-through">How It Works — Run Command Walk-through</h2>

<h3 id="run-command-1-dsc-install">Run Command 1: <code class="language-plaintext highlighter-rouge">DSC-Install</code></h3>

<p>Downloads the DSC v3 ZIP from the GitHub releases page, extracts it to <code class="language-plaintext highlighter-rouge">C:\Program Files\DSC\&lt;version&gt;</code>, and adds the directory to the system PATH.</p>

<p>Key parameters passed from Bicep:</p>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dscVersion</code></td>
      <td><code class="language-plaintext highlighter-rouge">3.2.0</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dscArch</code></td>
      <td><code class="language-plaintext highlighter-rouge">x86_64-pc-windows-msvc</code></td>
    </tr>
  </tbody>
</table>

<p>The download URL is constructed as:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://github.com/PowerShell/DSC/releases/download/v&lt;version&gt;/DSC-&lt;version&gt;-&lt;arch&gt;.zip
</code></pre></div></div>
<p><strong>Bicep code:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">
</span><span class="n">resource</span><span class="w"> </span><span class="nx">runCommandsInstallDSC</span><span class="w"> </span><span class="s1">'Microsoft.Compute/virtualMachines/runCommands@2025-11-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">parent:</span><span class="w"> </span><span class="nx">vm</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="s1">'DSC-Install'</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">
  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">timeoutInSeconds:</span><span class="w"> </span><span class="nx">900</span><span class="w">
    </span><span class="n">treatFailureAsDeploymentFailure:</span><span class="w"> </span><span class="nx">true</span><span class="w">
    </span><span class="n">parameters:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="n">name:</span><span class="w"> </span><span class="s1">'dscVersion'</span><span class="w">
        </span><span class="n">value:</span><span class="w"> </span><span class="s1">'3.2.0'</span><span class="w">
      </span><span class="p">}</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="n">name:</span><span class="w"> </span><span class="s1">'dscArch'</span><span class="w">
        </span><span class="n">value:</span><span class="w"> </span><span class="s1">'x86_64-pc-windows-msvc'</span><span class="w">
      </span><span class="p">}</span><span class="w">

    </span><span class="p">]</span><span class="w">
    </span><span class="n">source:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">script:</span><span class="w"> </span><span class="s1">'''
          #Requires -RunAsAdministrator

          param(
              [string]$dscVersion,
              [string]$dscArch
          )
          $downloadUrl = "https://github.com/PowerShell/DSC/releases/download/v$dscVersion/DSC-$dscVersion-$dscArch.zip"
          $installDir  = "C:\Program Files\DSC\$dscVersion"
          $zipPath     = Join-Path $env:TEMP "DSC-$dscVersion-$dscArch.zip"
          $logPath     = Join-Path $env:TEMP "DSC-$dscVersion-install_$(Get-Date -Format '</span><span class="nx">yyyyMMdd_HHmmss</span><span class="s1">').log"

          $ProgressPreference = '</span><span class="nx">SilentlyContinue</span><span class="s1">'

          function Write-Log {
              param([string]$Message, [ValidateSet('</span><span class="nx">INFO</span><span class="s1">','</span><span class="nx">WARN</span><span class="s1">','</span><span class="nx">ERROR</span><span class="s1">')]$Level = '</span><span class="nx">INFO</span><span class="s1">')
              $entry = "[$(Get-Date -Format '</span><span class="nx">yyyy-MM-dd</span><span class="w"> </span><span class="nx">HH:mm:ss</span><span class="s1">')] [$Level] $Message"
              Write-Host $entry
              Add-Content -Path $logPath -Value $entry
          }

          Write-Log "Log file: $logPath"
          Write-Log "Install directory: $installDir"
          Write-Log "Download URL: $downloadUrl"

          # Download
          Write-Log "Downloading DSC v$dscVersion..."
          try {
              Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing
              Write-Log "Download completed: $zipPath"
          } catch {
              Write-Log "Download failed: $_" -Level ERROR
              exit 1
          }

          # Extract
          if (-not (Test-Path $installDir)) {
              New-Item -ItemType Directory -Path $installDir -Force | Out-Null
              Write-Log "Created directory: $installDir"
          }
          Write-Log "Extracting to $installDir..."
          try {
              Expand-Archive -Path $zipPath -DestinationPath $installDir -Force
              Write-Log "Extraction completed."
          } catch {
              Write-Log "Extraction failed: $_" -Level ERROR
              exit 1
          }

          # Add to system PATH if not already present
          $currentPath = [Environment]::GetEnvironmentVariable('</span><span class="nx">Path</span><span class="s1">', '</span><span class="nx">Machine</span><span class="s1">')
          if ($currentPath -split '</span><span class="p">;</span><span class="s1">' -notcontains $installDir) {
              Write-Log "Adding $installDir to system PATH..."
              [Environment]::SetEnvironmentVariable('</span><span class="n">Path</span><span class="s1">', "$currentPath;$installDir", '</span><span class="nx">Machine</span><span class="s1">')
              Write-Log "System PATH updated. Restart your shell to apply changes."
          } else {
              Write-Log "$installDir is already in the system PATH." -Level WARN
          }

          # Cleanup temp file
          Remove-Item -Path $zipPath -Force
          Write-Log "Removed temporary file: $zipPath"

          Write-Log "DSC v$dscVersion installed successfully."
          Write-Log "Full log saved to: $logPath"

      '''</span><span class="w">

    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span></code></pre></div></div>

<h3 id="run-command-2-dsc-configuration">Run Command 2: <code class="language-plaintext highlighter-rouge">DSC-Configuration</code></h3>

<p>Runs <strong>after</strong> <code class="language-plaintext highlighter-rouge">DSC-Install</code> (<code class="language-plaintext highlighter-rouge">dependsOn</code>). It:</p>

<ol>
  <li>Adds the DSC install directory to the current session PATH so <code class="language-plaintext highlighter-rouge">dsc.exe</code> resolves immediately.</li>
  <li>Downloads <code class="language-plaintext highlighter-rouge">ps-script-certificate.dsc.yaml</code> from Blob Storage and pipes it to <code class="language-plaintext highlighter-rouge">dsc config set --file -</code>, capturing the JSON output.</li>
  <li>Extracts the certificate thumbprint from the DSC <code class="language-plaintext highlighter-rouge">afterState</code> output.</li>
  <li>Builds an inline parameters JSON block containing the thumbprint.</li>
  <li>Downloads <code class="language-plaintext highlighter-rouge">winrm.dsc.yaml</code> and applies it with <code class="language-plaintext highlighter-rouge">dsc config --parameters &lt;json&gt; set --file -</code>.</li>
</ol>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">
</span><span class="n">resource</span><span class="w"> </span><span class="nx">runCommandsApplyDSC</span><span class="w"> </span><span class="s1">'Microsoft.Compute/virtualMachines/runCommands@2025-11-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">parent:</span><span class="w"> </span><span class="nx">vm</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="s1">'DSC-Configuration'</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">
  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">timeoutInSeconds:</span><span class="w"> </span><span class="nx">900</span><span class="w">
    </span><span class="n">treatFailureAsDeploymentFailure:</span><span class="w"> </span><span class="nx">true</span><span class="w">
    </span><span class="n">parameters:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="n">name:</span><span class="w"> </span><span class="s1">'dscInstallDir'</span><span class="w">
        </span><span class="n">value:</span><span class="w"> </span><span class="s1">'3.2.0'</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
    </span><span class="n">source:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">script:</span><span class="w"> </span><span class="s1">'''

        param(
          [string]$dscInstallDir
        )

        $dscInstallDir = "C:\Program Files\DSC\$dscInstallDir"
        Write-Host "DSC for certificate "

        # Add DSC install dir to current user PATH if not already present
        $userPath = [Environment]::GetEnvironmentVariable('</span><span class="nx">Path</span><span class="s1">', '</span><span class="nx">User</span><span class="s1">')
        if ($userPath -split '</span><span class="p">;</span><span class="s1">' -notcontains $dscInstallDir) {
            [Environment]::SetEnvironmentVariable('</span><span class="n">Path</span><span class="s1">', "$userPath;$dscInstallDir", '</span><span class="nx">User</span><span class="s1">')
            Write-Host "Added $dscInstallDir to user PATH."
        }
        # Also update the current session so dsc.exe is resolvable immediately
        if ($env:PATH -split '</span><span class="p">;</span><span class="s1">' -notcontains $dscInstallDir) {
            $env:PATH = "$env:PATH;$dscInstallDir"
        }


        $contentYamlCert = Invoke-RestMethod -Uri "https://&lt;your-storage-account&gt;.blob.core.windows.net/dsc/ps-script-certificate.dsc.yaml"
        $result = $contentYamlCert | dsc config set --file - --output-format pretty-json
        $result = $result | ConvertFrom-Json

        $thumbprint = if ($result.results.result.afterState.output -is [System.Array]) {
            $result.results.result.afterState.output[0].Thumbprint
        } else {
            $result.results.result.afterState.output.Thumbprint
        }

        if (-not $thumbprint) {
            throw "Thumbprint was not found in DSC output."
        }


        Write-Host "DSC for certificate Thumbprint: " $thumbprint

        $inlineParams = @{
            parameters = @{
                certThumbprint = $thumbprint
            }
        } | ConvertTo-Json

        # dsc config --parameters $inlineParams get --file .\winrm.dsc.yaml
        # dsc config --parameters $inlineParams test --file .\winrm.dsc.yaml

        Write-Host "DSC - winrm HTTPS setup"

        $contentYamlWinrm = Invoke-RestMethod -Uri "https://&lt;your-storage-account&gt;.blob.core.windows.net/dsc/winrm.dsc.yaml"

        $contentYamlWinrm | dsc config --parameters $inlineParams set --file -


      '''</span><span class="w">

    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="n">dependsOn:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="n">runCommandsInstallDSC</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<hr />

<h2 id="verifying-the-result">Verifying the Result</h2>

<p>After the deployment completes, RDP into the VM and run:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check WinRM listener</span><span class="w">
</span><span class="n">Get-WSManInstance</span><span class="w"> </span><span class="nt">-ResourceURI</span><span class="w"> </span><span class="nx">winrm/config/listener</span><span class="w"> </span><span class="nt">-Enumerate</span><span class="w">

</span><span class="c"># Confirm the HTTPS listener is present</span><span class="w">
</span><span class="n">winrm</span><span class="w"> </span><span class="nx">enumerate</span><span class="w"> </span><span class="nx">winrm/config/listener</span><span class="w">
</span></code></pre></div></div>

<p>You should see an HTTPS listener on port 5986 with the thumbprint of the self-signed certificate.</p>

<hr />

<h2 id="summary">Summary</h2>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th>Tool</th>
      <th>What happens</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Local</td>
      <td>Prepare DSC YAML documents</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Azure PowerShell / Portal</td>
      <td>Create Storage Account and container</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Azure PowerShell</td>
      <td>Upload YAML blobs</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Text editor</td>
      <td>Update blob URLs in <code class="language-plaintext highlighter-rouge">main.bicep</code></td>
    </tr>
    <tr>
      <td>5</td>
      <td>Azure PowerShell</td>
      <td>Run <code class="language-plaintext highlighter-rouge">New-AzResourceGroupDeployment</code></td>
    </tr>
    <tr>
      <td>Auto</td>
      <td>Bicep Run Command</td>
      <td>Install DSC v3 on VM</td>
    </tr>
    <tr>
      <td>Auto</td>
      <td>Bicep Run Command</td>
      <td>Apply certificate + WinRM config via DSC v3</td>
    </tr>
  </tbody>
</table>

<p>This pattern is composable: swap the YAML documents for any other DSC v3 configuration and the same Bicep skeleton handles the bootstrap and delivery.</p>

<hr />

<h2 id="related-posts">Related Posts</h2>

<ul>
  <li><a href="/2026/05/13/dscv3-and-powershell-setup-winrm/">Setting Up WinRM HTTPS Listener with DSC v3 and PowerShell</a></li>
  <li><a href="/2026/02/09/hot-to-use-dscv3-in-azure-devops/">How to Use AzureDevOpsDscv3 in Azure DevOps Pipelines</a></li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://github.com/PowerShell/DSC">Microsoft DSCv3 Github</a></li>
  <li><a href="https://github.com/dsccommunity/ActiveDirectoryDsc">DSC community Github</a></li>
  <li><a href="https://github.com/mimachniak/sysopslife-scripts">Blog code examples Github</a></li>
</ul>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="Azure" /><category term="DSC" /><category term="IaaC" /><category term="Bicep" /><category term="DSCv3" /><category term="WinRM" /><category term="Security" /><summary type="html"><![CDATA[End-to-end walkthrough: provision an Azure Windows VM with Bicep, install DSC v3 via a Run Command extension, store DSC YAML documents in Azure Blob Storage, and apply them on the VM to create a self-signed certificate and configure a WinRM HTTPS listener — all in a single deployment..]]></summary></entry><entry><title type="html">Setting Up WinRM HTTPS Listener with DSC v3 and PowerShell</title><link href="https://mimachniak.github.io/sysopslife/2026/05/13/dscv3-and-powershell-setup-winrm/" rel="alternate" type="text/html" title="Setting Up WinRM HTTPS Listener with DSC v3 and PowerShell" /><published>2026-05-13T00:00:00+02:00</published><updated>2026-05-13T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2026/05/13/dscv3-and-powershell-setup-winrm</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2026/05/13/dscv3-and-powershell-setup-winrm/"><![CDATA[<p><img src="/assets/images/DSC/dsc-flow.PNG" alt="" /></p>

<h1 id="setting-up-a-winrm-https-listener-with-dsc-v3-and-powershell">Setting Up a WinRM HTTPS Listener with DSC v3 and PowerShell</h1>

<h2 id="introduction">Introduction</h2>

<p>Windows Remote Management (WinRM) is the Microsoft implementation of the WS-Management protocol. By default WinRM operates over HTTP on port 5985 — adequate for internal lab environments, but not suitable for production. Enabling the <strong>HTTPS listener</strong> on port 5986 encrypts the entire management channel using TLS and requires a certificate bound to the listener.</p>

<p>Configuring this manually involves multiple steps: creating or importing a certificate, writing registry values under a dynamically named GUID key, opening the firewall, and ensuring the WinRM service is running. One missed step means the listener is broken or insecure.</p>

<p>This post automates the whole process with <strong>Microsoft DSC v3</strong> — a standalone, declarative engine that applies YAML configuration documents and supports native Windows resources out of the box.</p>

<hr />

<h2 id="solution-overview">Solution Overview</h2>

<p>The configuration is split into two focused DSC YAML documents and one PowerShell orchestrator:</p>

<table>
  <thead>
    <tr>
      <th>File</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ps-script-certificate.dsc.yaml</code></td>
      <td>Creates a self-signed certificate in the local machine store</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">winrm.dsc.yaml</code></td>
      <td>Configures registry keys, firewall rule, and WinRM service</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">winrm.ps1</code></td>
      <td>Orchestrates both documents, passing the certificate thumbprint between them</td>
    </tr>
  </tbody>
</table>

<p>The split is intentional: the certificate step runs under the <strong>current</strong> security context (the default), while the WinRM registry and firewall configuration requires an <strong>elevated (restricted)</strong> security context.</p>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<p><strong>DSC v3 installed:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nt">--id</span><span class="w"> </span><span class="nx">Microsoft.DSC</span><span class="w">
</span></code></pre></div></div>

<p><strong>Verify resources are available:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="nx">Microsoft.Windows/Registry</span><span class="w">
</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="nx">Microsoft.Windows/FirewallRuleList</span><span class="w">
</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="nx">Microsoft.Windows/Service</span><span class="w">
</span><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="nx">Microsoft.DSC.Transitional/PowerShellScript</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="phase-1--create-the-self-signed-certificate">Phase 1 — Create the Self-Signed Certificate</h2>

<h3 id="ps-script-certificatedscyaml"><code class="language-plaintext highlighter-rouge">ps-script-certificate.dsc.yaml</code></h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>

<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span>

<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC.Transitional/PowerShellScript</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">CreateSelfSignedCertificate</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">GetScript</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">$dnsName = $env:COMPUTERNAME</span>
          <span class="s">$cert = Get-ChildItem Cert:\LocalMachine\My |</span>
              <span class="s">Where-Object {</span>
                  <span class="s">$_.Subject -eq "CN=$dnsName"</span>
              <span class="s">}</span>

          <span class="s">return @{</span>
              <span class="s">Result = if ($cert) { "Present" } else { "Absent" }</span>
              <span class="s">Thumbprint = if ($cert) { $cert.Thumbprint } else { $null }</span>
          <span class="s">}</span>
      <span class="na">TestScript</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">$dnsName = $env:COMPUTERNAME</span>
          <span class="s">$cert = Get-ChildItem Cert:\LocalMachine\My |</span>
              <span class="s">Where-Object {</span>
                  <span class="s">$_.Subject -eq "CN=$dnsName"</span>
              <span class="s">}</span>

          <span class="s">return ($null -ne $cert)</span>
      <span class="na">SetScript</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">$dnsName = $env:COMPUTERNAME</span>
        <span class="s">$cert = Get-ChildItem Cert:\LocalMachine\My |</span>
              <span class="s">Where-Object {</span>
                  <span class="s">$_.Subject -eq "CN=$dnsName"</span>
              <span class="s">}</span>
        <span class="s">if (-not $cert) {</span>
            <span class="s">$cert = New-SelfSignedCertificate -DnsName $dnsName -CertStoreLocation "Cert:\LocalMachine\My" -KeyLength 2048 -HashAlgorithm SHA256</span>
            <span class="s">}</span>
        <span class="s">return @{ Thumbprint = $cert.Thumbprint }</span>
</code></pre></div></div>

<h3 id="resource-microsoftdsctransitionalpowershellscript">Resource: <code class="language-plaintext highlighter-rouge">Microsoft.DSC.Transitional/PowerShellScript</code></h3>

<p>This is a <strong>transitional resource</strong> — a bridge between DSC v3’s native model and imperative PowerShell logic. It implements the three-method contract that every DSC resource must expose:</p>

<table>
  <thead>
    <tr>
      <th>Script property</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GetScript</code></td>
      <td>Returns the current state. Queries <code class="language-plaintext highlighter-rouge">Cert:\LocalMachine\My</code> for a certificate whose subject matches <code class="language-plaintext highlighter-rouge">CN=$env:COMPUTERNAME</code>. Returns <code class="language-plaintext highlighter-rouge">Present</code>/<code class="language-plaintext highlighter-rouge">Absent</code> and the thumbprint if found.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TestScript</code></td>
      <td>Returns <code class="language-plaintext highlighter-rouge">$true</code> if the certificate already exists, <code class="language-plaintext highlighter-rouge">$false</code> if DSC needs to run <code class="language-plaintext highlighter-rouge">SetScript</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SetScript</code></td>
      <td>Creates the certificate with <code class="language-plaintext highlighter-rouge">New-SelfSignedCertificate</code> using SHA256, a 2048-bit key, and the computer name as the DNS name. Returns the thumbprint so it can be captured by the orchestrator.</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Note:</strong> <code class="language-plaintext highlighter-rouge">requiredSecurityContext: current</code> means this document can be applied by any user context. Creating a certificate in <code class="language-plaintext highlighter-rouge">Cert:\LocalMachine\My</code> still requires administrative rights in practice, but the DSC security context flag here is set to <code class="language-plaintext highlighter-rouge">current</code> since the operation does not touch system-wide WinRM configuration.</p>
</blockquote>

<hr />

<h2 id="phase-2--configure-the-winrm-https-listener">Phase 2 — Configure the WinRM HTTPS Listener</h2>

<h3 id="winrmdscyaml"><code class="language-plaintext highlighter-rouge">winrm.dsc.yaml</code></h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>

<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">restricted</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">certThumbprint</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s">Thumbprint of the certificate to use for the WinRM HTTPS listener.</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">341B58BC25D182D844B64BF7FC52D1D751625960"</span>

<span class="na">resources</span><span class="pi">:</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - Address</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
      <span class="na">valueName</span><span class="pi">:</span> <span class="s">Address</span>
      <span class="na">valueData</span><span class="pi">:</span>
        <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - Transport</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
      <span class="na">valueName</span><span class="pi">:</span> <span class="s">Transport</span>
      <span class="na">valueData</span><span class="pi">:</span>
        <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HTTPS"</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - Port</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
      <span class="na">valueName</span><span class="pi">:</span> <span class="s">Port</span>
      <span class="na">valueData</span><span class="pi">:</span>
        <span class="na">DWord</span><span class="pi">:</span> <span class="m">5986</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - certThumbprint</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
      <span class="na">valueName</span><span class="pi">:</span> <span class="s">certThumbprint</span>
      <span class="na">valueData</span><span class="pi">:</span>
        <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('certThumbprint')]"</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM HTTPS listener - hostname</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Registry</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">keyPath</span><span class="pi">:</span> <span class="s">HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\*+HTTPS</span>
      <span class="na">valueName</span><span class="pi">:</span> <span class="s">hostname</span>
      <span class="na">valueData</span><span class="pi">:</span>
        <span class="na">String</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[envvar('COMPUTERNAME')]"</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM firewall rules</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/FirewallRuleList</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">rules</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Windows Remote Management (HTTPS-In)</span>
          <span class="na">description</span><span class="pi">:</span> <span class="s">Allow inbound TCP traffic on port 5986 for WinRM HTTPS.</span>
          <span class="na">applicationName</span><span class="pi">:</span> <span class="s">System</span>
          <span class="na">protocol</span><span class="pi">:</span> <span class="m">6</span>
          <span class="na">localPorts</span><span class="pi">:</span> <span class="s1">'</span><span class="s">5986'</span>
          <span class="na">direction</span><span class="pi">:</span> <span class="s">Inbound</span>
          <span class="na">action</span><span class="pi">:</span> <span class="s">Allow</span>
          <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
          <span class="na">profiles</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">Domain</span>
            <span class="pi">-</span> <span class="s">Private</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM Service</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Service</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM</span>
      <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">status</span><span class="pi">:</span> <span class="s">Running</span>
      <span class="na">startType</span><span class="pi">:</span> <span class="s">Automatic</span>
</code></pre></div></div>

<h3 id="resource-microsoftwindowsregistry">Resource: <code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code></h3>

<p>The <strong>Registry</strong> resource is DSC v3’s native way to manage Windows registry keys and values. Each instance targets one specific value.</p>

<p>WinRM stores its listener configuration under:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Listener\&lt;GUID&gt;+HTTPS
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">&lt;GUID&gt;</code> portion is generated randomly when the listener is first created and is not predictable. DSC v3’s Registry resource supports the <code class="language-plaintext highlighter-rouge">*</code> wildcard in <code class="language-plaintext highlighter-rouge">keyPath</code>, so <code class="language-plaintext highlighter-rouge">*+HTTPS</code> matches the listener key regardless of its GUID, making the configuration portable across machines.</p>

<p>Key properties used across all five Registry resource instances:</p>

<table>
  <thead>
    <tr>
      <th>Property</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">_exist: true</code></td>
      <td>Ensures the key/value must exist (set) rather than be absent (delete)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">keyPath</code></td>
      <td>Full registry path including hive, key, and wildcard</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">valueName</code></td>
      <td>The registry value name to manage</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">valueData</code></td>
      <td>Typed value — <code class="language-plaintext highlighter-rouge">String</code> for text, <code class="language-plaintext highlighter-rouge">DWord</code> for 32-bit integer</td>
    </tr>
  </tbody>
</table>

<p><strong>Values configured:</strong></p>

<table>
  <thead>
    <tr>
      <th><code class="language-plaintext highlighter-rouge">valueName</code></th>
      <th>Type</th>
      <th>Value</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Address</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">*</code></td>
      <td>Bind to all IP addresses</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Transport</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">HTTPS</code></td>
      <td>HTTPS transport</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Port</code></td>
      <td>DWord</td>
      <td><code class="language-plaintext highlighter-rouge">5986</code></td>
      <td>Standard WinRM HTTPS port</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">certThumbprint</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">[parameters('certThumbprint')]</code></td>
      <td>Certificate bound to the listener</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hostname</code></td>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">[envvar('COMPUTERNAME')]</code></td>
      <td>Machine hostname</td>
    </tr>
  </tbody>
</table>

<p>Notice two DSC v3 expression functions in use:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">[parameters('certThumbprint')]</code></strong> — resolves the <code class="language-plaintext highlighter-rouge">certThumbprint</code> parameter, which is supplied at runtime from the orchestrator script.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">[envvar('COMPUTERNAME')]</code></strong> — reads the <code class="language-plaintext highlighter-rouge">COMPUTERNAME</code> environment variable at apply time, making the configuration self-adapting to any machine.</li>
</ul>

<h3 id="resource-microsoftwindowsfirewallrulelist">Resource: <code class="language-plaintext highlighter-rouge">Microsoft.Windows/FirewallRuleList</code></h3>

<p>The <strong>FirewallRuleList</strong> resource manages one or more Windows Firewall rules as a single unit. It maps directly to Windows Defender Firewall rules and supports both creating new rules and enforcing the desired state of existing ones.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM firewall rules</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/FirewallRuleList</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">rules</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Windows Remote Management (HTTPS-In)</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s">Allow inbound TCP traffic on port 5986 for WinRM HTTPS.</span>
        <span class="na">applicationName</span><span class="pi">:</span> <span class="s">System</span>
        <span class="na">protocol</span><span class="pi">:</span> <span class="m">6</span>        <span class="c1"># TCP</span>
        <span class="na">localPorts</span><span class="pi">:</span> <span class="s1">'</span><span class="s">5986'</span>
        <span class="na">direction</span><span class="pi">:</span> <span class="s">Inbound</span>
        <span class="na">action</span><span class="pi">:</span> <span class="s">Allow</span>
        <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
        <span class="na">profiles</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">Domain</span>
          <span class="pi">-</span> <span class="s">Private</span>
</code></pre></div></div>

<p>Key fields:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Value</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">protocol</code></td>
      <td><code class="language-plaintext highlighter-rouge">6</code></td>
      <td>TCP (IANA protocol number)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">localPorts</code></td>
      <td><code class="language-plaintext highlighter-rouge">5986</code></td>
      <td>Destination port for WinRM HTTPS</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">direction</code></td>
      <td><code class="language-plaintext highlighter-rouge">Inbound</code></td>
      <td>Allow incoming connections</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">action</code></td>
      <td><code class="language-plaintext highlighter-rouge">Allow</code></td>
      <td>Permit traffic</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">profiles</code></td>
      <td><code class="language-plaintext highlighter-rouge">Domain</code>, <code class="language-plaintext highlighter-rouge">Private</code></td>
      <td>Active on domain-joined and private networks</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>The <code class="language-plaintext highlighter-rouge">Public</code> profile is intentionally excluded — exposing WinRM on public networks is a security risk.</p>
</blockquote>

<h3 id="resource-microsoftwindowsservice">Resource: <code class="language-plaintext highlighter-rouge">Microsoft.Windows/Service</code></h3>

<p>The <strong>Service</strong> resource ensures the WinRM Windows service is running and set to start automatically:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM Service</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/Service</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">WinRM</span>
    <span class="na">_exist</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">status</span><span class="pi">:</span> <span class="s">Running</span>
    <span class="na">startType</span><span class="pi">:</span> <span class="s">Automatic</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Property</th>
      <th>Value</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">name</code></td>
      <td><code class="language-plaintext highlighter-rouge">WinRM</code></td>
      <td>Windows service name</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">_exist</code></td>
      <td><code class="language-plaintext highlighter-rouge">true</code></td>
      <td>Service must exist</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">status</code></td>
      <td><code class="language-plaintext highlighter-rouge">Running</code></td>
      <td>Service must be in the running state</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">startType</code></td>
      <td><code class="language-plaintext highlighter-rouge">Automatic</code></td>
      <td>Service starts with Windows</td>
    </tr>
  </tbody>
</table>

<h3 id="security-context-restricted">Security Context: <code class="language-plaintext highlighter-rouge">restricted</code></h3>

<p><code class="language-plaintext highlighter-rouge">winrm.dsc.yaml</code> declares <code class="language-plaintext highlighter-rouge">requiredSecurityContext: restricted</code>, which is the default and signals that the configuration must be applied by an elevated (administrative) process. Writing to <code class="language-plaintext highlighter-rouge">HKLM</code>, creating firewall rules, and managing services all require administrator rights — the security context declaration makes this requirement explicit and prevents accidental application from a non-elevated shell.</p>

<hr />

<h2 id="orchestrator-winrmps1">Orchestrator: <code class="language-plaintext highlighter-rouge">winrm.ps1</code></h2>

<p>The PowerShell script ties both documents together. It:</p>

<ol>
  <li>Applies the certificate configuration and extracts the thumbprint from the DSC output</li>
  <li>Passes the thumbprint as an inline parameter to the WinRM listener configuration</li>
</ol>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"DSC for certificate"</span><span class="w">

</span><span class="nv">$result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ps-script-certificate.dsc.yaml</span><span class="w"> </span><span class="nt">--output-format</span><span class="w"> </span><span class="nx">pretty-json</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">20</span><span class="w">

</span><span class="nv">$thumbprint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$result</span><span class="o">.</span><span class="nf">results</span><span class="o">.</span><span class="nf">result</span><span class="o">.</span><span class="nf">afterState</span><span class="o">.</span><span class="nf">output</span><span class="w"> </span><span class="o">-is</span><span class="w"> </span><span class="p">[</span><span class="n">System.Array</span><span class="p">])</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$result</span><span class="o">.</span><span class="nf">results</span><span class="o">.</span><span class="nf">result</span><span class="o">.</span><span class="nf">afterState</span><span class="o">.</span><span class="n">output</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="nf">Thumbprint</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$result</span><span class="o">.</span><span class="nf">results</span><span class="o">.</span><span class="nf">result</span><span class="o">.</span><span class="nf">afterState</span><span class="o">.</span><span class="nf">output</span><span class="o">.</span><span class="nf">Thumbprint</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$thumbprint</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Thumbprint was not found in DSC output."</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Certificate thumbprint: </span><span class="nv">$thumbprint</span><span class="s2">"</span><span class="w">

</span><span class="nv">$inlineParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
        </span><span class="nx">certThumbprint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$thumbprint</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w">

</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"DSC - WinRM HTTPS setup"</span><span class="w">

</span><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nt">--parameters</span><span class="w"> </span><span class="nv">$inlineParams</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\winrm.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<p><strong>Key points:</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">dsc config set --output-format pretty-json</code> returns structured JSON, including the <code class="language-plaintext highlighter-rouge">afterState</code> for each resource. The <code class="language-plaintext highlighter-rouge">PowerShellScript</code> resource returns the <code class="language-plaintext highlighter-rouge">SetScript</code> output in <code class="language-plaintext highlighter-rouge">.results.result.afterState.output</code>.</li>
  <li>The thumbprint is extracted by checking whether the output is an array (multiple results) or a single object, making the extraction robust.</li>
  <li>The inline parameters JSON is passed via <code class="language-plaintext highlighter-rouge">dsc config --parameters &lt;json&gt;</code> which injects it at runtime, replacing <code class="language-plaintext highlighter-rouge">[parameters('certThumbprint')]</code> in the YAML.</li>
  <li>A guard (<code class="language-plaintext highlighter-rouge">throw</code>) stops execution immediately if the thumbprint is missing rather than proceeding with an empty value.</li>
</ul>

<hr />

<h2 id="applying-the-configuration">Applying the Configuration</h2>

<p>Run the orchestrator from the directory containing all three files:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Test only — no changes applied</span><span class="w">
</span><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ps-script-certificate.dsc.yaml</span><span class="w">

</span><span class="c"># Full apply</span><span class="w">
</span><span class="o">.</span><span class="n">\winrm.ps1</span><span class="w">
</span></code></pre></div></div>

<p>To verify the listener was created:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winrm</span><span class="w"> </span><span class="nx">enumerate</span><span class="w"> </span><span class="nx">winrm/config/listener</span><span class="w">

</span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nx">WSMan:\localhost\Listener</span><span class="w">

</span></code></pre></div></div>

<p>Expected output should include a listener with <code class="language-plaintext highlighter-rouge">Transport = HTTPS</code> and <code class="language-plaintext highlighter-rouge">Port = 5986</code>.</p>

<p>To test the connection from a remote machine:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Test-WSMan</span><span class="w"> </span><span class="nt">-ComputerName</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">hostname</span><span class="err">&gt;</span><span class="w"> </span><span class="nt">-UseSSL</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="summary">Summary</h2>

<table>
  <thead>
    <tr>
      <th>Resource</th>
      <th>Type</th>
      <th>What it configures</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.DSC.Transitional/PowerShellScript</code></td>
      <td>Transitional</td>
      <td>Self-signed TLS certificate in <code class="language-plaintext highlighter-rouge">Cert:\LocalMachine\My</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Registry</code> (×5)</td>
      <td>Native</td>
      <td>WinRM listener registry values under <code class="language-plaintext highlighter-rouge">WSMAN\Listener\*+HTTPS</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/FirewallRuleList</code></td>
      <td>Native</td>
      <td>Inbound TCP 5986 rule on Domain and Private profiles</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.Windows/Service</code></td>
      <td>Native</td>
      <td>WinRM service — Running, Automatic start</td>
    </tr>
  </tbody>
</table>

<p>DSC v3 makes this multi-step process repeatable, auditable, and idempotent. The split between certificate creation and listener configuration, the use of <code class="language-plaintext highlighter-rouge">[parameters()]</code> and <code class="language-plaintext highlighter-rouge">[envvar()]</code> expressions, and the <code class="language-plaintext highlighter-rouge">requiredSecurityContext</code> declaration together produce a configuration that is both portable and security-aware.</p>

<hr />

<h2 id="references">References</h2>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/powershell/dsc/overview">DSC v3 documentation</a></li>
  <li><a href="https://learn.microsoft.com/en-us/powershell/dsc/reference/microsoft/windows/registry/overview">Microsoft.Windows/Registry resource</a></li>
  <li><a href="https://learn.microsoft.com/en-us/powershell/dsc/reference/microsoft/windows/service/overview">Microsoft.Windows/Service resource</a></li>
  <li><a href="https://learn.microsoft.com/en-us/powershell/dsc/reference/microsoft/windows/firewallrulelist/overview">Microsoft.Windows/FirewallRuleList resource</a></li>
  <li>
    <p><a href="https://learn.microsoft.com/en-us/windows/win32/winrm/installation-and-configuration-for-windows-remote-management">WinRM HTTPS setup — Microsoft Docs</a></p>
  </li>
  <li><a href="https://github.com/PowerShell/DSC">Microsoft DSCv3 Github</a></li>
  <li><a href="https://github.com/dsccommunity/ActiveDirectoryDsc">DSC community Github</a></li>
  <li><a href="https://github.com/mimachniak/sysopslife-scripts">Blog code examples Github</a></li>
</ul>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="DSC" /><category term="PowerShell" /><category term="IaaC" /><category term="Code" /><category term="WinRM" /><category term="Security" /><summary type="html"><![CDATA[This post shows how to automate a secure WinRM HTTPS listener configuration using Microsoft DSC v3. The solution combines native DSC v3 resources — Registry, FirewallRuleList, Service — with the PowerShellScript transitional resource to provision a self-signed certificate and wire it to the listener, all orchestrated from a single PowerShell script.]]></summary></entry><entry><title type="html">Microsoft DSCv3 build and deploy classic resources ActiveDirectoryDSC</title><link href="https://mimachniak.github.io/sysopslife/2026/05/06/dscv3-build-and-deploy-classic-resources-activedirectory-dsc/" rel="alternate" type="text/html" title="Microsoft DSCv3 build and deploy classic resources ActiveDirectoryDSC" /><published>2026-05-06T00:00:00+02:00</published><updated>2026-05-06T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2026/05/06/dscv3-build-and-deploy-classic-resources-activedirectory-dsc</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2026/05/06/dscv3-build-and-deploy-classic-resources-activedirectory-dsc/"><![CDATA[<p><img src="/assets/images/DSC/dsc-flow.PNG" alt="" /></p>

<h1 id="deploying-active-directory-with-microsoft-dsc-v3--a-configuration-as-code-approach">Deploying Active Directory with Microsoft DSC v3 — A Configuration-as-Code Approach</h1>

<h2 id="introduction">Introduction</h2>

<p>Microsoft Desired State Configuration (DSC) v3 is a cross-platform, declarative configuration management engine built in Rust. Unlike its PowerShell-based predecessors (DSC v1 and v2), DSC v3 is a standalone binary (<code class="language-plaintext highlighter-rouge">dsc.exe</code>) that orchestrates resources defined in YAML documents. It supports the full PowerShell DSC adapter ecosystem, enabling teams to configure Windows infrastructure — including Active Directory — using pure YAML files.</p>

<p>This post walks through a complete, modular Active Directory deployment built on DSC v3, covering every configuration file, parameter file, and the commands used to apply them.</p>

<hr />

<h2 id="why-dsc-v3-for-active-directory">Why DSC v3 for Active Directory?</h2>

<p>Traditional AD deployment relies on PowerShell scripts or manual steps that are difficult to audit, version, and reproduce. DSC v3 brings:</p>

<ul>
  <li><strong>Declarative YAML</strong> — describe <em>what</em> you want, not <em>how</em> to do it</li>
  <li><strong>Idempotency</strong> — running the same configuration multiple times produces the same result</li>
  <li><strong>Modularity</strong> — split large configurations into focused, reusable include files</li>
  <li><strong>Parameter separation</strong> — keep environment-specific values (domain names, passwords) out of the configuration files</li>
  <li><strong>Built-in dependency management</strong> — <code class="language-plaintext highlighter-rouge">dependsOn</code> ensures resources are applied in the correct order</li>
</ul>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<p>Before running any configuration, ensure the following are in place on the target Domain Controller:</p>

<p><strong>Search DSC v3:</strong></p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">search</span><span class="w"> </span><span class="nx">DesiredStateConfiguration</span><span class="w">
</span><span class="n">Name</span><span class="w">                              </span><span class="nx">Id</span><span class="w">            </span><span class="nx">Version</span><span class="w"> </span><span class="nx">Match</span><span class="w">                          </span><span class="nx">Source</span><span class="w">
</span><span class="o">----------------------------------------------------------------------------------------------</span><span class="w">
</span><span class="n">DesiredStateConfiguration</span><span class="w">         </span><span class="nx">9NVTPZWRC6KQ</span><span class="w">  </span><span class="nx">Unknown</span><span class="w">                                </span><span class="nx">msstore</span><span class="w">
</span><span class="n">DesiredStateConfiguration-Preview</span><span class="w"> </span><span class="nx">9PCX3HX4HZ0Z</span><span class="w">  </span><span class="nx">Unknown</span><span class="w">                                </span><span class="nx">msstore</span><span class="w">
</span><span class="n">DSC</span><span class="w"> </span><span class="nx">v3</span><span class="w">                            </span><span class="nx">Microsoft.DSC</span><span class="w"> </span><span class="nx">3.2.0</span><span class="w">   </span><span class="nx">Tag:</span><span class="w"> </span><span class="nx">desiredstateconfiguration</span><span class="w"> </span><span class="nx">winget</span><span class="w">

</span></code></pre></div></div>

<p><strong>Install DSC v3:</strong></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nt">--id</span><span class="w"> </span><span class="nx">9PCX3HX4HZ0Z</span><span class="w"> </span><span class="nt">--source</span><span class="w"> </span><span class="nx">msstore</span><span class="w">
</span></code></pre></div></div>

<p><strong>Install the ActiveDirectoryDsc PowerShell module:</strong></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">ActiveDirectoryDsc</span><span class="w"> </span><span class="nt">-Repository</span><span class="w"> </span><span class="nx">PSGallery</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span></code></pre></div></div>

<p><strong>Verify DSC v3 is available and can discover resources:</strong></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">resource</span><span class="w"> </span><span class="nx">list</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="project-file-structure">Project File Structure</h2>

<p>The configuration is split into a layered directory structure to separate concerns:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>config/ADS/
├── ads-root-forest.dsc.yaml          # Stage 1 – Forest/domain creation
├── ads-root.dsc.parameters.yaml      # Parameters for root forest deployment
├── ads-child-forest.dsc.yaml         # Stage 1 (optional) – Child domain creation
│
└── ADS-Include/
    ├── ads.main.dsc.yaml             # Stage 2 – Orchestrator (Include entries)
    │
    ├── config/
    │   ├── ads.ou.dsc.yaml           # OU hierarchy definition
    │   ├── ads.admin-users.dsc.yaml  # Administrative user accounts
    │   ├── ads.groups.dsc.yaml       # Delegation security groups
    │   ├── ads.groups-members.dsc.yaml    # Group membership assignments
    │   ├── ads.ou-delegation.dsc.yaml     # OU permission entries (ACLs)
    │   └── ads.finegrained-password-policy.dsc.yaml  # PSO policies
    │
    └── parameters/
        ├── ads.parameters.yaml            # Domain root path parameter
        └── ads.admin-user.parameters.yaml # Admin user credentials
</code></pre></div></div>

<hr />

<h2 id="stage-1--forest-and-domain-creation">Stage 1 — Forest and Domain Creation</h2>

<h3 id="ads-root-forestdscyaml"><code class="language-plaintext highlighter-rouge">ads-root-forest.dsc.yaml</code></h3>

<p>This is the entry point for bringing up the first domain controller. It installs the required Windows features and then creates the AD forest using <code class="language-plaintext highlighter-rouge">ActiveDirectoryDsc/ADDomain</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ads-root-forest-configuration</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
  <span class="na">description</span><span class="pi">:</span> <span class="s">Global Active Directory configuration for forest and domain creation</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">SafemodeAdministratorPassword</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">secureObject</span>
  <span class="na">Credential</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">secureObject</span>
  <span class="na">DomainName</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s">contoso.com</span>
  <span class="na">DomainNetBiosName</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s">contoso</span>
  <span class="na">ForestMode</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s">WinThreshold</span>   <span class="c1"># Win2008, Win2008R2, Win2012, Win2012R2, WinThreshold, Win2025</span>
  <span class="na">SuppressReboot</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">bool</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">resources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Windows Features Install ADS</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">PSDesiredStateConfiguration/WindowsFeature</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">AD-Domain-Services</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Windows Features Install ADS RSAT</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">PSDesiredStateConfiguration/WindowsFeature</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">RSAT-AD-PowerShell</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Windows Features Install AD DS Tools</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">PSDesiredStateConfiguration/WindowsFeature</span>
    <span class="na">properties</span><span class="pi">:</span>
      <span class="na">ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">name</span><span class="pi">:</span> <span class="s">RSAT-ADDS</span>
      <span class="na">IncludeAllSubFeature</span><span class="pi">:</span> <span class="no">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Forest</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADDomain</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">DomainName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('DomainName')]"</span>
      <span class="na">DomainNetBiosName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('DomainNetBiosName')]"</span>
      <span class="na">SafemodeAdministratorPassword</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('SafemodeAdministratorPassword')]"</span>
      <span class="na">Credential</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('Credential')]"</span>
      <span class="na">ForestMode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('ForestMode')]"</span>
      <span class="na">SuppressReboot</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('SuppressReboot')]"</span> <span class="c1"># no reboot rquired</span>
    <span class="na">dependsOn</span><span class="pi">:</span> 
      <span class="pi">-</span> <span class="s2">"</span><span class="s">[resourceId('PSDesiredStateConfiguration/WindowsFeature','Windows</span><span class="nv"> </span><span class="s">Features</span><span class="nv"> </span><span class="s">Install</span><span class="nv"> </span><span class="s">ADS')]"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">[resourceId('PSDesiredStateConfiguration/WindowsFeature','Windows</span><span class="nv"> </span><span class="s">Features</span><span class="nv"> </span><span class="s">Install</span><span class="nv"> </span><span class="s">ADS</span><span class="nv"> </span><span class="s">RSAT')]"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">[resourceId('PSDesiredStateConfiguration/WindowsFeature','Windows</span><span class="nv"> </span><span class="s">Features</span><span class="nv"> </span><span class="s">Install</span><span class="nv"> </span><span class="s">AD</span><span class="nv"> </span><span class="s">DS</span><span class="nv"> </span><span class="s">Tools')]"</span>
</code></pre></div></div>

<p><strong>Key resources:</strong></p>

<table>
  <thead>
    <tr>
      <th>Resource name</th>
      <th>DSC type</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Windows Features Install ADS</td>
      <td><code class="language-plaintext highlighter-rouge">PSDesiredStateConfiguration/WindowsFeature</code></td>
      <td>Installs <code class="language-plaintext highlighter-rouge">AD-Domain-Services</code></td>
    </tr>
    <tr>
      <td>Windows Features Install ADS RSAT</td>
      <td><code class="language-plaintext highlighter-rouge">PSDesiredStateConfiguration/WindowsFeature</code></td>
      <td>Installs <code class="language-plaintext highlighter-rouge">RSAT-AD-PowerShell</code></td>
    </tr>
    <tr>
      <td>Windows Features Install AD DS Tools</td>
      <td><code class="language-plaintext highlighter-rouge">PSDesiredStateConfiguration/WindowsFeature</code></td>
      <td>Installs <code class="language-plaintext highlighter-rouge">RSAT-ADDS</code> with all sub-features</td>
    </tr>
    <tr>
      <td>Active Directory Forest</td>
      <td><code class="language-plaintext highlighter-rouge">ActiveDirectoryDsc/ADDomain</code></td>
      <td>Promotes server to DC and creates the forest</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">ADDomain</code> resource depends on all three <code class="language-plaintext highlighter-rouge">WindowsFeature</code> resources, so DSC applies them in sequence automatically.</p>

<p><strong><code class="language-plaintext highlighter-rouge">ads-root.dsc.parameters.yaml</code></strong> — supplies the sensitive values at runtime, keeping them out of the configuration file:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="na">Credential</span><span class="pi">:</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">AdminTest</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">Password</span>
  <span class="na">SafemodeAdministratorPassword</span><span class="pi">:</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">AdminTest</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">AdminTest</span>
  <span class="na">DomainName</span><span class="pi">:</span> <span class="s">contoso.com</span>
  <span class="na">DomainNetBiosName</span><span class="pi">:</span> <span class="s">contoso</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Security note:</strong> In Organization, replace plain-text passwords with secrets retrieved from Azure Key Vault or a secrets manager. Never commit credential files to source control.</p>
</blockquote>

<p><strong>Deploy command:</strong></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--parameters-file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads-root.dsc.parameters.yaml</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads-root-forest.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<hr />

<h3 id="ads-child-forestdscyaml"><code class="language-plaintext highlighter-rouge">ads-child-forest.dsc.yaml</code></h3>

<p>Used when adding a child domain to an existing forest. The structure mirrors the root forest file but adds <code class="language-plaintext highlighter-rouge">DomainType</code> and <code class="language-plaintext highlighter-rouge">DomainMode</code> parameters:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainType</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s">ChildDomain</span>   <span class="c1"># TreeDomain or ChildDomain</span>
  <span class="na">DomainMode</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s">WinThreshold</span>
</code></pre></div></div>

<p>This allows the same pattern to deploy both root and child domains by varying the parameters.</p>

<hr />

<h2 id="stage-2--ad-structure-with-include-based-orchestration">Stage 2 — AD Structure with Include-Based Orchestration</h2>

<p>Once the domain exists, the second stage configures its internal structure. Rather than one monolithic YAML, the project uses the <code class="language-plaintext highlighter-rouge">Microsoft.DSC/Include</code> resource type to compose smaller, focused files.</p>

<h3 id="ads-includeadsmaindscyaml"><code class="language-plaintext highlighter-rouge">ADS-Include/ads.main.dsc.yaml</code></h3>

<p>This is the single entry point for Stage 2. It does nothing itself — it delegates to six sub-configurations via <code class="language-plaintext highlighter-rouge">Include</code>, order of deployment is base on yaml files:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">resources</span><span class="pi">:</span>

<span class="c1"># Deploy in order </span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Ou structure</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC/Include</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">configurationFile</span><span class="pi">:</span> <span class="s">config\ads.ou.dsc.yaml</span>
    <span class="na">parametersFile</span><span class="pi">:</span> <span class="s">parameters\ads.parameters.yaml</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Administrative User</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC/Include</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">configurationFile</span><span class="pi">:</span> <span class="s">config\ads.admin-users.dsc.yaml</span>
    <span class="na">parametersFile</span><span class="pi">:</span> <span class="s">parameters\ads.admin-user.parameters.yaml</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Groups Management</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC/Include</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">configurationFile</span><span class="pi">:</span> <span class="s">config\ads.groups.dsc.yaml</span>
    <span class="na">parametersFile</span><span class="pi">:</span> <span class="s">parameters\ads.parameters.yaml</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Groups Membership</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC/Include</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">configurationFile</span><span class="pi">:</span> <span class="s">config\ads.groups-members.dsc.yaml</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Groups OU Delegation</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC/Include</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">configurationFile</span><span class="pi">:</span> <span class="s">config\ads.ou-delegation.dsc.yaml</span>
    <span class="na">parametersFile</span><span class="pi">:</span> <span class="s">parameters\ads.parameters.yaml</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Password Policy</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">Microsoft.DSC/Include</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">configurationFile</span><span class="pi">:</span> <span class="s">config\ads.finegrained-password-policy.dsc.yaml</span>
    <span class="na">parametersFile</span><span class="pi">:</span> <span class="s">parameters\ads.parameters.yaml</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Microsoft.DSC/Include</code> resource loads an external configuration file and optionally merges a parameters file into it before execution. This pattern is the key to keeping each concern in its own file without repeating boilerplate.</p>

<hr />

<h2 id="sub-configuration-files">Sub-Configuration Files</h2>

<h3 id="configadsoudscyaml--organizational-unit-hierarchy"><code class="language-plaintext highlighter-rouge">config/ads.ou.dsc.yaml</code> — Organizational Unit Hierarchy</h3>

<p>Defines the entire OU tree under the domain root. The structure follows a tiered model:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Organization (root)
├── Accounts
│   ├── Users
│   ├── Users.NoSync
├── Devices
│   └── Servers
│       ├── Tier0
│       ├── Tier1
│       └── Tier2
│   └── Computers
└── Groups
    ├── Delegation
    └── Distribution
</code></pre></div></div>

<p>Each OU uses <code class="language-plaintext highlighter-rouge">ActiveDirectoryDsc/ADOrganizationalUnit</code> and declares its parent via <code class="language-plaintext highlighter-rouge">dependsOn</code>, using the <code class="language-plaintext highlighter-rouge">resourceId()</code> function to reference sibling resources:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ads-groups-ou-configuration</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
  <span class="na">description</span><span class="pi">:</span> <span class="s">Active Directory configuration of defualt groups</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span> <span class="c1"># this is the default and just used as an example indicating this config works for admins and non-admins</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
<span class="na">resources</span><span class="pi">:</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Root Organizational Unit</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADOrganizationalUnit</span>
  <span class="na">properties</span><span class="pi">:</span> 
    <span class="na">Name</span><span class="pi">:</span> <span class="s">Organization</span>
    <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('DomainRootPath')]"</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Organization</span><span class="nv"> </span><span class="s">OU</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">All</span><span class="nv"> </span><span class="s">organizational</span><span class="nv"> </span><span class="s">objects"</span>
    <span class="na">ProtectedFromAccidentalDeletion</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">ensure</span><span class="pi">:</span> <span class="s">Present</span>


<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Accounts Organizational Unit</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADOrganizationalUnit</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">Name</span><span class="pi">:</span> <span class="s">Accounts</span>
    <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Accounts</span><span class="nv"> </span><span class="s">OU</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">all</span><span class="nv"> </span><span class="s">user</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">service</span><span class="nv"> </span><span class="s">accounts"</span>
    <span class="na">ProtectedFromAccidentalDeletion</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">ensure</span><span class="pi">:</span> <span class="s">Present</span>
    <span class="na">dependsOn</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">[resourceId('ActiveDirectoryDsc/ADOrganizationalUnit','Active</span><span class="nv"> </span><span class="s">Directory</span><span class="nv"> </span><span class="s">Root</span><span class="nv"> </span><span class="s">Organizational</span><span class="nv"> </span><span class="s">Unit')]"</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Users Organizational Unit</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADOrganizationalUnit</span>
  <span class="na">properties</span><span class="pi">:</span> 
    <span class="na">Name</span><span class="pi">:</span> <span class="s">Users</span>
    <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Accounts,','OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">OU</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">Users</span><span class="nv"> </span><span class="s">accounts</span><span class="nv"> </span><span class="s">that</span><span class="nv"> </span><span class="s">can</span><span class="nv"> </span><span class="s">be</span><span class="nv"> </span><span class="s">synchonized</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">Microsoft</span><span class="nv"> </span><span class="s">365</span><span class="nv"> </span><span class="s">or</span><span class="nv"> </span><span class="s">used</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">on-premises</span><span class="nv"> </span><span class="s">authentication"</span>
    <span class="na">ProtectedFromAccidentalDeletion</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">ensure</span><span class="pi">:</span> <span class="s">Present</span>
    <span class="na">dependsOn</span><span class="pi">:</span> 
      <span class="pi">-</span> <span class="s2">"</span><span class="s">[resourceId('ActiveDirectoryDsc/ADOrganizationalUnit','Active</span><span class="nv"> </span><span class="s">Directory</span><span class="nv"> </span><span class="s">Accounts</span><span class="nv"> </span><span class="s">Organizational</span><span class="nv"> </span><span class="s">Unit')]"</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">concat()</code> function builds the LDAP path dynamically from the <code class="language-plaintext highlighter-rouge">DomainRootPath</code> parameter, making the file fully portable across domains.</p>

<p>All OUs have <code class="language-plaintext highlighter-rouge">ProtectedFromAccidentalDeletion: true</code> set to prevent inadvertent removal.</p>

<hr />

<h3 id="configadsadmin-usersdscyaml--administrative-user-accounts"><code class="language-plaintext highlighter-rouge">config/ads.admin-users.dsc.yaml</code> — Administrative User Accounts</h3>

<p>Creates privileged user accounts and places them in the correct OU:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainName</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">contoso.com"</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
  <span class="na">Password</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">secureObject</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Password</code> parameter is typed as <code class="language-plaintext highlighter-rouge">secureObject</code>, meaning it accepts a <code class="language-plaintext highlighter-rouge">{ username, password }</code> structure without exposing the value in logs. The user is placed in <code class="language-plaintext highlighter-rouge">OU=Users.Special.NoSync</code> — the dedicated OU for privileged accounts that must not synchronize to Microsoft 365:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ads-admin-users-configuration</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
  <span class="na">description</span><span class="pi">:</span> <span class="s">Active Directory configuration for administrative user</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span> <span class="c1"># this is the default and just used as an example indicating this config works for admins and non-admins</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainName</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">contoso.com"</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
  <span class="na">Password</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">secureObject</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory admin - michal.mmachniak</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADUser</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
    <span class="na">UserName</span><span class="pi">:</span> <span class="s">michal.mmachniak</span>
    <span class="na">CommonName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Michal</span><span class="nv"> </span><span class="s">Machniak"</span>
    <span class="na">UserPrincipalName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('michal.mmachniak@',</span><span class="nv"> </span><span class="s">parameters('DomainName'))]"</span>
    <span class="na">Password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('Password')]"</span>
    <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Users.Special.NoSync,OU=Accounts,OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
    <span class="na">PasswordNeverResets</span><span class="pi">:</span> <span class="no">true</span> <span class="c1"># Each deployment won't setup new password</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">parameters/ads.admin-user.parameters.yaml</code></strong> provides the runtime values for this config:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="na">Password</span><span class="pi">:</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">admin</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">P@SSWord!@!</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
  <span class="na">DomainName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">contoso.com"</span>
</code></pre></div></div>

<hr />

<h3 id="configadsgroupsdscyaml--delegation-security-groups"><code class="language-plaintext highlighter-rouge">config/ads.groups.dsc.yaml</code> — Delegation Security Groups</h3>

<p>Creates Universal Security Groups in the <code class="language-plaintext highlighter-rouge">OU=Delegation,OU=Groups,OU=Organization</code> container. Each group corresponds to a delegation scope on a specific OU:</p>

<table>
  <thead>
    <tr>
      <th>Group name</th>
      <th>Delegation target</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OU-Organization-FullControl</code></td>
      <td>Organization OU (full control)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OU-Organization-Accounts-RWD</code></td>
      <td>Accounts OU (Read/Write/Delete)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OU-Organization-Accounts-Users-RWD</code></td>
      <td>Users OU</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OU-Organization-Accounts-Users.NoSync-RWD</code></td>
      <td>Users.NoSync OU</td>
    </tr>
  </tbody>
</table>

<p>All groups follow the same pattern:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ads-groups-ou-configuration</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
  <span class="na">description</span><span class="pi">:</span> <span class="s">Active Directory configuration of defualt groups</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span> <span class="c1"># this is the default and just used as an example indicating this config works for admins and non-admins</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
<span class="na">resources</span><span class="pi">:</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Organization OU</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADGroup</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">GroupName</span><span class="pi">:</span> <span class="s">OU-Organization-FullControl</span>
    <span class="na">GroupScope</span><span class="pi">:</span> <span class="s">Universal</span>
    <span class="na">Category</span><span class="pi">:</span> <span class="s">Security</span>
    <span class="na">Description</span><span class="pi">:</span> <span class="s">Delegation group for OU Organization with full control permissions</span>
    <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Delegation,OU=Groups,OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
    <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
</code></pre></div></div>

<p>Using Universal scope allows these groups to work across trusts in multi-domain forest scenarios.</p>

<hr />

<h3 id="configadsgroups-membersdscyaml--group-membership"><code class="language-plaintext highlighter-rouge">config/ads.groups-members.dsc.yaml</code> — Group Membership</h3>

<p>Assigns accounts to groups using <code class="language-plaintext highlighter-rouge">MembersToInclude</code>, which is additive and non-destructive (it does not remove existing members):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory - Domain Admins group membership</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADGroup</span>
  <span class="na">properties</span><span class="pi">:</span>
    <span class="na">GroupName</span><span class="pi">:</span> <span class="s">Domain Admins</span>
    <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
    <span class="na">MembersToInclude</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">michal.mmachniak</span>
</code></pre></div></div>

<p>This config intentionally has no parameters file — the group names and member accounts are static by design for privileged group membership, reducing the risk of accidental misconfiguration.</p>

<hr />

<h3 id="configadsou-delegationdscyaml--ou-permission-entries"><code class="language-plaintext highlighter-rouge">config/ads.ou-delegation.dsc.yaml</code> — OU Permission Entries</h3>

<p>Applies ACLs to OUs using <code class="language-plaintext highlighter-rouge">ActiveDirectoryDsc/ADObjectPermissionEntry</code>. Each entry grants the corresponding delegation group the required access:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ads-groups-ou-configuration</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
  <span class="na">description</span><span class="pi">:</span> <span class="s">Active Directory configuration for organizational units delegations</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span> <span class="c1"># this is the default and just used as an example indicating this config works for admins and non-admins</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
<span class="na">resources</span><span class="pi">:</span>

<span class="c1"># This configuration defines a security group for delegating permissions on the Organization OU in Active Directory. The group is created with full control permissions on the OU, allowing members of the group to manage the OU and its contents effectively.</span>


  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Organization OU Delegation Group Permissions</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADObjectPermissionEntry</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
      <span class="na">IdentityReference</span><span class="pi">:</span> <span class="s">OU-Organization-FullControl</span>
      <span class="na">ActiveDirectoryRights</span><span class="pi">:</span> 
        <span class="pi">-</span> <span class="s">GenericAll</span>
      <span class="na">AccessControlType</span><span class="pi">:</span> <span class="s">Allow</span>
      <span class="na">ObjectType</span><span class="pi">:</span> <span class="s">00000000-0000-0000-0000-000000000000</span>
      <span class="na">ActiveDirectorySecurityInheritance</span><span class="pi">:</span> <span class="s">Descendents</span>
      <span class="na">InheritedObjectType</span><span class="pi">:</span> <span class="s">00000000-0000-0000-0000-000000000000</span>

<span class="c1"># This configuration defines a security group for delegating permissions on the Organization-Accounts OU in Active Directory. The group is created with full control permissions on the OU, allowing members of the group to manage user accounts and other objects within the OU effectively.</span>


  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Organization-Accounts OU Delegation Group Permissions Descendents</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADObjectPermissionEntry</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Accounts,OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
      <span class="na">IdentityReference</span><span class="pi">:</span> <span class="s">OU-Organization-Accounts-RWD</span>
      <span class="na">ActiveDirectoryRights</span><span class="pi">:</span> 
        <span class="pi">-</span> <span class="s">GenericAll</span>
      <span class="na">AccessControlType</span><span class="pi">:</span> <span class="s">Allow</span>
      <span class="na">ObjectType</span><span class="pi">:</span> <span class="s">00000000-0000-0000-0000-000000000000</span>
      <span class="na">ActiveDirectorySecurityInheritance</span><span class="pi">:</span> <span class="s">Descendents</span>
      <span class="na">InheritedObjectType</span><span class="pi">:</span> <span class="s">User</span> <span class="c1"># User"</span>


  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Organization-Accounts-Users OU Delegation Group Permissions Descendents</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADObjectPermissionEntry</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Users,OU=Accounts,OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
      <span class="na">IdentityReference</span><span class="pi">:</span> <span class="s">OU-Organization-Accounts-Users-RWD</span>
      <span class="na">ActiveDirectoryRights</span><span class="pi">:</span> 
        <span class="pi">-</span> <span class="s">GenericAll</span>
      <span class="na">AccessControlType</span><span class="pi">:</span> <span class="s">Allow</span>
      <span class="na">ObjectType</span><span class="pi">:</span> <span class="s">00000000-0000-0000-0000-000000000000</span>
      <span class="na">ActiveDirectorySecurityInheritance</span><span class="pi">:</span> <span class="s">Descendents</span>
      <span class="na">InheritedObjectType</span><span class="pi">:</span> <span class="s">User</span> <span class="c1"># User</span>

      <span class="c1">## User.NoSync</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Organization-Accounts-Users.NoSync OU Delegation Group Permissions Descendents</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADObjectPermissionEntry</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Users.NoSync,OU=Accounts,OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
      <span class="na">IdentityReference</span><span class="pi">:</span> <span class="s">OU-Organization-Accounts-Users.NoSync-RWD</span>
      <span class="na">ActiveDirectoryRights</span><span class="pi">:</span> 
        <span class="pi">-</span> <span class="s">GenericAll</span>
      <span class="na">AccessControlType</span><span class="pi">:</span> <span class="s">Allow</span>
      <span class="na">ObjectType</span><span class="pi">:</span> <span class="s">00000000-0000-0000-0000-000000000000</span>
      <span class="na">ActiveDirectorySecurityInheritance</span><span class="pi">:</span> <span class="s">Descendents</span>
      <span class="na">InheritedObjectType</span><span class="pi">:</span> <span class="s">User</span> <span class="c1"># User</span>

      
<span class="c1"># Computer objects in the Organization-Devices OU are delegated to a security group with permissions to manage computer accounts and other objects within the OU effectively.</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory Organization-Devices OU Delegation Group Permissions</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADObjectPermissionEntry</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">Ensure</span><span class="pi">:</span> <span class="s">Present</span>
      <span class="na">Path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[concat('OU=Devices,OU=Organization,',</span><span class="nv"> </span><span class="s">parameters('DomainRootPath'))]"</span>
      <span class="na">IdentityReference</span><span class="pi">:</span> <span class="s">OU-Organization-Devices-RWD</span>
      <span class="na">ActiveDirectoryRights</span><span class="pi">:</span> 
        <span class="pi">-</span> <span class="s">GenericAll</span>
      <span class="na">AccessControlType</span><span class="pi">:</span> <span class="s">Allow</span>
      <span class="na">ObjectType</span><span class="pi">:</span> <span class="s">00000000-0000-0000-0000-000000000000</span>
      <span class="na">ActiveDirectorySecurityInheritance</span><span class="pi">:</span> <span class="s">Descendents</span>
      <span class="na">InheritedObjectType</span><span class="pi">:</span> <span class="s">Computer</span> <span class="c1"># Computer objects</span>


   
</code></pre></div></div>

<p>For granular OUs (like <code class="language-plaintext highlighter-rouge">Users</code>, <code class="language-plaintext highlighter-rouge">Users.NoSync</code>), the <code class="language-plaintext highlighter-rouge">InheritedObjectType</code> is set to <code class="language-plaintext highlighter-rouge">User</code>, restricting the ACE to apply only to user object descendants:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">ActiveDirectorySecurityInheritance</span><span class="pi">:</span> <span class="s">Descendents</span>
<span class="na">InheritedObjectType</span><span class="pi">:</span> <span class="s">User</span>
</code></pre></div></div>

<p>This enforces the principle of least privilege — delegation group members can only manage user objects in their designated OU subtree.</p>

<hr />

<h3 id="configadsfinegrained-password-policydscyaml--fine-grained-password-policies"><code class="language-plaintext highlighter-rouge">config/ads.finegrained-password-policy.dsc.yaml</code> — Fine-Grained Password Policies</h3>

<p>Applies Password Settings Objects (PSOs) to privileged groups, enforcing stricter password rules than the default domain policy:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">ads-finegrained-password-policy-configuration</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
  <span class="na">description</span><span class="pi">:</span> <span class="s">Active Directory configuration for AD FineGrained Password Policy</span>
  <span class="na">Microsoft.DSC</span><span class="pi">:</span>
    <span class="na">requiredSecurityContext</span><span class="pi">:</span> <span class="s">current</span> <span class="c1"># this is the default and just used as an example indicating this config works for admins and non-admins</span>

<span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">defaultValue</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
<span class="na">resources</span><span class="pi">:</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory ADFineGrainedPasswordPolicy - Domain Admins</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADFineGrainedPasswordPolicy</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">Name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">DomainAdmins'</span>
      <span class="na">DisplayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Domain</span><span class="nv"> </span><span class="s">Admins</span><span class="nv"> </span><span class="s">Password</span><span class="nv"> </span><span class="s">Policy'</span>
      <span class="na">Description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">This</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">Fine</span><span class="nv"> </span><span class="s">Grained</span><span class="nv"> </span><span class="s">Password</span><span class="nv"> </span><span class="s">Policy</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">Domain</span><span class="nv"> </span><span class="s">Admins'</span>
      <span class="na">Subjects</span><span class="pi">:</span> 
        <span class="pi">-</span> <span class="s">Domain Admins</span>
      <span class="na">ComplexityEnabled</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">LockoutDuration</span><span class="pi">:</span> <span class="s1">'</span><span class="s">00:30:00'</span>
      <span class="na">LockoutObservationWindow</span><span class="pi">:</span> <span class="s1">'</span><span class="s">00:30:00'</span>
      <span class="na">LockoutThreshold</span><span class="pi">:</span> <span class="m">5</span>
      <span class="na">MaxPasswordAge</span><span class="pi">:</span> <span class="s1">'</span><span class="s">90.00:00:00'</span>
      <span class="na">MinPasswordAge</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.00:00:00'</span>
      <span class="na">MinPasswordLength</span><span class="pi">:</span> <span class="m">15</span>
      <span class="na">PasswordHistoryCount</span><span class="pi">:</span> <span class="m">6</span>
      <span class="na">ReversibleEncryptionEnabled</span><span class="pi">:</span> <span class="no">false</span>
      <span class="na">ProtectedFromAccidentalDeletion</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">Precedence</span><span class="pi">:</span> <span class="m">10</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Active Directory ADFineGrainedPasswordPolicy - Enterprise Admins</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ActiveDirectoryDsc/ADFineGrainedPasswordPolicy</span>
    <span class="na">properties</span><span class="pi">:</span> 
      <span class="na">Name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">EnterpriseAdmins'</span>
      <span class="na">DisplayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Enterprise</span><span class="nv"> </span><span class="s">Admins</span><span class="nv"> </span><span class="s">Password</span><span class="nv"> </span><span class="s">Policy'</span>
      <span class="na">Description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">This</span><span class="nv"> </span><span class="s">is</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">Fine</span><span class="nv"> </span><span class="s">Grained</span><span class="nv"> </span><span class="s">Password</span><span class="nv"> </span><span class="s">Policy</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">Enterprise</span><span class="nv"> </span><span class="s">Admins'</span>
      <span class="na">Subjects</span><span class="pi">:</span> 
        <span class="pi">-</span> <span class="s">Enterprise Admins</span>
      <span class="na">ComplexityEnabled</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">LockoutDuration</span><span class="pi">:</span> <span class="s1">'</span><span class="s">00:30:00'</span>
      <span class="na">LockoutObservationWindow</span><span class="pi">:</span> <span class="s1">'</span><span class="s">00:30:00'</span>
      <span class="na">LockoutThreshold</span><span class="pi">:</span> <span class="m">5</span>
      <span class="na">MaxPasswordAge</span><span class="pi">:</span> <span class="s1">'</span><span class="s">90.00:00:00'</span>
      <span class="na">MinPasswordAge</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.00:00:00'</span>
      <span class="na">MinPasswordLength</span><span class="pi">:</span> <span class="m">15</span>
      <span class="na">PasswordHistoryCount</span><span class="pi">:</span> <span class="m">6</span>
      <span class="na">ReversibleEncryptionEnabled</span><span class="pi">:</span> <span class="no">false</span>
      <span class="na">ProtectedFromAccidentalDeletion</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">Precedence</span><span class="pi">:</span> <span class="m">11</span>
</code></pre></div></div>

<p>Two PSOs are defined:</p>

<table>
  <thead>
    <tr>
      <th>PSO name</th>
      <th>Applied to</th>
      <th>Min length</th>
      <th>Max age</th>
      <th>Precedence</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DomainAdmins</code></td>
      <td>Domain Admins</td>
      <td>15</td>
      <td>90 days</td>
      <td>10</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">EnterpriseAdmins</code></td>
      <td>Enterprise Admins</td>
      <td>15</td>
      <td>90 days</td>
      <td>11</td>
    </tr>
  </tbody>
</table>

<p>Lower <code class="language-plaintext highlighter-rouge">Precedence</code> numbers take priority when multiple PSOs apply to the same user.</p>

<hr />

<h2 id="parameter-files">Parameter Files</h2>

<h3 id="parametersadsparametersyaml"><code class="language-plaintext highlighter-rouge">parameters/ads.parameters.yaml</code></h3>

<p>Shared by most sub-configurations to supply the domain LDAP root path:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
</code></pre></div></div>

<h3 id="parametersadsadmin-userparametersyaml"><code class="language-plaintext highlighter-rouge">parameters/ads.admin-user.parameters.yaml</code></h3>

<p>Provides all values required by the admin users configuration:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="na">Password</span><span class="pi">:</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">AdminTest</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">Password</span>
  <span class="na">DomainRootPath</span><span class="pi">:</span> <span class="s2">"</span><span class="s">DC=contoso,DC=com"</span>
  <span class="na">DomainName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">contoso.com"</span>
</code></pre></div></div>

<hr />

<h2 id="deployment-workflow">Deployment Workflow</h2>

<h3 id="step-1--validate-configuration-dry-run">Step 1 — Validate configuration (dry-run)</h3>

<p>Before applying anything, use <code class="language-plaintext highlighter-rouge">test</code> mode to see what is out of compliance:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nt">--parameters-file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads-root.dsc.parameters.yaml</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads-root-forest.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<p>For the Stage 2 orchestrator:</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nt">--parameters-file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ADS-Include\parameters\ads.parameters.yaml</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ADS-Include\ads.main.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<h3 id="step-2--apply-stage-1-create-the-forest">Step 2 — Apply Stage 1: Create the forest</h3>

<p>Run this on the first server that will become the Domain Controller. The server will be promoted and — if <code class="language-plaintext highlighter-rouge">SuppressReboot: false</code> — will reboot automatically.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nt">--parameters-file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads-root.dsc.parameters.yaml</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads-root-forest.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<h3 id="step-3--apply-stage-2-configure-ad-structure">Step 3 — Apply Stage 2: Configure AD structure</h3>

<p>After the DC is up and the domain is functional, run the main orchestrator from within the <code class="language-plaintext highlighter-rouge">ADS-Include</code> directory:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-Location</span><span class="w"> </span><span class="o">.</span><span class="nx">\ADS-Include</span><span class="w">

</span><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads.main.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Include</code> resource resolves <code class="language-plaintext highlighter-rouge">configurationFile</code> paths relative to the location of <code class="language-plaintext highlighter-rouge">ads.main.dsc.yaml</code>, so the working directory must match.</p>

<h3 id="step-4--enable-trace-level-logging-troubleshooting">Step 4 — Enable trace-level logging (troubleshooting)</h3>

<p>If any resource fails, enable detailed output with <code class="language-plaintext highlighter-rouge">--trace-level</code>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nt">--trace-level</span><span class="w"> </span><span class="nx">trace</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nt">--parameters-file</span><span class="w"> </span><span class="o">.</span><span class="nx">\parameters\ads.parameters.yaml</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="se">`
</span><span class="w">  </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads.main.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<p>Available trace levels: <code class="language-plaintext highlighter-rouge">error</code>, <code class="language-plaintext highlighter-rouge">warn</code>, <code class="language-plaintext highlighter-rouge">info</code>, <code class="language-plaintext highlighter-rouge">debug</code>, <code class="language-plaintext highlighter-rouge">trace</code></p>

<h3 id="step-5--re-validate-after-apply">Step 5 — Re-validate after apply</h3>

<p>Confirm the configuration converged successfully:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dsc</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">test</span><span class="w"> </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">\ads.main.dsc.yaml</span><span class="w">
</span></code></pre></div></div>

<p>A fully converged configuration returns <code class="language-plaintext highlighter-rouge">InDesiredState: true</code> for every resource.</p>

<hr />

<h2 id="dsc-v3-concepts-used-in-this-project">DSC v3 Concepts Used in This Project</h2>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">$schema</code></td>
      <td>Declares the DSC v3 document schema — required in every config file</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Microsoft.DSC/Include</code></td>
      <td>Composes external YAML files into one logical configuration</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parametersFile</code></td>
      <td>Merges a separate YAML parameter document into the included config</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parameters()</code></td>
      <td>Function that references a declared parameter value</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">concat()</code></td>
      <td>Builds strings dynamically from literals and parameter values</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">resourceId()</code></td>
      <td>Returns a reference to another resource, used in <code class="language-plaintext highlighter-rouge">dependsOn</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dependsOn</code></td>
      <td>Enforces ordering — a resource waits until its dependencies complete</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">secureObject</code></td>
      <td>Parameter type for credential objects — value is masked in logs</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">requiredSecurityContext: current</code></td>
      <td>Indicates the config can run under the current user context</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="recommended-execution-checklist">Recommended Execution Checklist</h2>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />DSC v3 (<code class="language-plaintext highlighter-rouge">dsc.exe</code>) installed and in <code class="language-plaintext highlighter-rouge">PATH</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">ActiveDirectoryDsc</code> module installed via PSGallery</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Running as a Domain Admin (or local Administrator for Stage 1)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Passwords in parameter files replaced with vault-sourced values before Organization use</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">dsc config test</code> passes cleanly before <code class="language-plaintext highlighter-rouge">dsc config set</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Stage 1 (forest creation) completes and DC reboots before running Stage 2</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Stage 2 executed from within the <code class="language-plaintext highlighter-rouge">ADS-Include</code> directory</li>
</ul>

<hr />

<h2 id="summary">Summary</h2>

<p>This project demonstrates how DSC v3 can manage the full lifecycle of an Active Directory environment — from forest creation to a Organization-ready OU structure, delegation model, and password policies — using nothing but declarative YAML files.</p>

<p>The <code class="language-plaintext highlighter-rouge">Microsoft.DSC/Include</code> pattern is the architectural cornerstone: it allows each concern (OUs, users, groups, delegation, password policy) to live in its own file while a single orchestrator file (<code class="language-plaintext highlighter-rouge">ads.main.dsc.yaml</code>) ties them together. Combined with parameter files, the same set of configurations can target any domain simply by swapping the <code class="language-plaintext highlighter-rouge">DomainRootPath</code> and <code class="language-plaintext highlighter-rouge">DomainName</code> values.</p>

<p>This approach brings Active Directory configuration into the modern infrastructure-as-code world — auditable, reproducible, and version-controlled.</p>

<h2 id="links-for-more-resources">Links for more resources</h2>

<ul>
  <li><a href="https://github.com/PowerShell/DSC">Microsoft DSCv3</a></li>
  <li><a href="https://github.com/dsccommunity/ActiveDirectoryDsc">DSC community</a></li>
</ul>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="Azure" /><category term="DSC" /><category term="IaaC" /><category term="Code" /><summary type="html"><![CDATA[This blog post explores how to use Microsoft Desired State Configuration v3 (DSC v3) to build, deploy, and manage infrastructure while reusing classic PowerShell DSC resources, focusing on the ActiveDirectoryDSC module. The article demonstrates how DSC v3 integrates with legacy DSC ecosystems using adapter resources, allowing organizations to continue using proven modules like **ActiveDirectoryDSC** without refactoring. This module provides automation capabilities for Active Directory, including domain deployment, domain controllers, trusts, users, groups, and organizational units.]]></summary></entry><entry><title type="html">How to Use AzureDevOpsDscv3 in Azure DevOps Pipelines</title><link href="https://mimachniak.github.io/sysopslife/2026/02/09/hot-to-use-dscv3-in-azure-devops/" rel="alternate" type="text/html" title="How to Use AzureDevOpsDscv3 in Azure DevOps Pipelines" /><published>2026-02-09T00:00:00+01:00</published><updated>2026-02-09T00:00:00+01:00</updated><id>https://mimachniak.github.io/sysopslife/2026/02/09/hot-to-use-dscv3-in-azure-devops</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2026/02/09/hot-to-use-dscv3-in-azure-devops/"><![CDATA[<h1 id="how-to-use-azuredevopsdscv3-in-azure-devops-pipelines">How to Use AzureDevOpsDscv3 in Azure DevOps Pipelines</h1>

<p>This post shows a real-life example for using <a href="https://github.com/mimachniak/AzureDevOpsDscv3">AzureDevOpsDscv3</a> inside an Azure DevOps pipeline. The goal is to define Azure DevOps projects, users, and groups as code, then apply the configuration in a controlled pipeline.</p>

<h2 id="scenario">Scenario</h2>

<p>You are onboarding a new product team. Every environment must have:</p>

<ul>
  <li>A standard project created from a Git template</li>
  <li>A group created from Entra ID</li>
  <li>A baseline set of users</li>
</ul>

<p>Instead of clicking in the UI, you use DSC v3 to declare everything, and the pipeline applies it consistently across environments.</p>

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li>Azure DevOps organization and a PAT with permissions to manage projects and users</li>
  <li>A self-hosted or Microsoft-hosted Windows agent</li>
  <li>PowerShell 7 and DSC v3 (<code class="language-plaintext highlighter-rouge">dsc</code> CLI)</li>
  <li>The AzureDevOpsDscv3 module from the PowerShell Gallery</li>
</ul>

<p>In the pipeline we will install the required modules, so the agent stays clean and the process is repeatable.</p>

<h2 id="repository-layout">Repository Layout</h2>

<p>Example structure you can place in your repo:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├─ dsc
│  └─ ado.dsc.yaml
├─ pipelines
│  └─ ado-dsc.yml
└─ README.md
</code></pre></div></div>

<h2 id="dsc-v3-configuration-file">DSC v3 Configuration File</h2>

<p>Below is a simplified config that creates a project, adds a user, and links an Entra ID group. Save it as <code class="language-plaintext highlighter-rouge">dsc/ado.dsc.yaml</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">$schema</span><span class="pi">:</span> <span class="s">https://aka.ms/dsc/schemas/v3/bundled/config/document.json</span>
<span class="na">parameters</span><span class="pi">:</span>
<span class="err">	</span><span class="na">Token</span><span class="pi">:</span>
<span class="err">		</span><span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
<span class="na">		defaultValue</span><span class="pi">:</span> <span class="s">PAT-Token</span>
<span class="na">resources</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Configure Azure DevOps</span>
<span class="na">	type</span><span class="pi">:</span> <span class="s">Microsoft.Windows/WindowsPowerShell</span>
<span class="na">	properties</span><span class="pi">:</span>
<span class="err">		</span><span class="na">resources</span><span class="pi">:</span>
<span class="err">		</span><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create project</span>
<span class="na">			type</span><span class="pi">:</span> <span class="s">AzureDevOpsDscv3/ProjectResource</span>
<span class="na">			properties</span><span class="pi">:</span>
<span class="err">				</span><span class="na">Organization</span><span class="pi">:</span> <span class="s">ExampleOrganization</span>
<span class="na">				ProjectName</span><span class="pi">:</span> <span class="s">Contoso-Platform</span>
<span class="na">				Description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Project</span><span class="nv"> </span><span class="s">created</span><span class="nv"> </span><span class="s">via</span><span class="nv"> </span><span class="s">DSC</span><span class="nv"> </span><span class="s">v3"</span>
<span class="err">				</span><span class="na">pat</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('Token')]"</span>
<span class="err">				</span><span class="na">SourceControlType</span><span class="pi">:</span> <span class="s">Git</span>
<span class="na">				Ensure</span><span class="pi">:</span> <span class="s">Present</span>
<span class="na">		- name</span><span class="pi">:</span> <span class="s">Add user</span>
<span class="na">			type</span><span class="pi">:</span> <span class="s">AzureDevOpsDscv3/OrganizationUserResource</span>
<span class="na">			properties</span><span class="pi">:</span>
<span class="err">				</span><span class="na">UserPrincipalName</span><span class="pi">:</span> <span class="s">dev1@contoso.com</span>
<span class="na">				Organization</span><span class="pi">:</span> <span class="s">ExampleOrganization</span>
<span class="na">				AccessLevel</span><span class="pi">:</span> <span class="s">Basic</span>
<span class="na">				Ensure</span><span class="pi">:</span> <span class="s">Present</span>
<span class="na">				pat</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('Token')]"</span>
<span class="err">		</span><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Add group</span>
<span class="na">			type</span><span class="pi">:</span> <span class="s">AzureDevOpsDscv3/OrganizationGroupResource</span>
<span class="na">			properties</span><span class="pi">:</span>
<span class="err">				</span><span class="na">GroupOriginId</span><span class="pi">:</span> <span class="s">00000000-0000-0000-0000-000000000000</span>
<span class="na">				GroupDisplayName</span><span class="pi">:</span> <span class="s">Contoso-DevTeam</span>
<span class="na">				Organization</span><span class="pi">:</span> <span class="s">ExampleOrganization</span>
<span class="na">				AccessLevel</span><span class="pi">:</span> <span class="s">Basic</span>
<span class="na">				Ensure</span><span class="pi">:</span> <span class="s">Present</span>
<span class="na">				pat</span><span class="pi">:</span> <span class="s2">"</span><span class="s">[parameters('Token')]"</span>
</code></pre></div></div>

<p>Notes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GroupOriginId</code> is the Entra ID group object ID.</li>
  <li><code class="language-plaintext highlighter-rouge">pat</code> is sourced from the <code class="language-plaintext highlighter-rouge">Token</code> parameter, which we inject in the pipeline.</li>
</ul>

<h2 id="azure-devops-pipeline">Azure DevOps Pipeline</h2>

<p>Create a pipeline YAML file, for example <code class="language-plaintext highlighter-rouge">pipelines/ado-dsc.yml</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">trigger</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">main</span>

<span class="na">pool</span><span class="pi">:</span>
<span class="err">	</span><span class="na">vmImage</span><span class="pi">:</span> <span class="s">windows-latest</span>

<span class="na">variables</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">DscConfigPath</span>
<span class="na">	value</span><span class="pi">:</span> <span class="s">dsc/ado.dsc.yaml</span>

<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">checkout</span><span class="pi">:</span> <span class="s">self</span>

<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">	displayName</span><span class="pi">:</span> <span class="s">Install DSC v3 and AzureDevOpsDscv3</span>
<span class="na">	inputs</span><span class="pi">:</span>
<span class="err">		</span><span class="na">pwsh</span><span class="pi">:</span> <span class="no">true</span>
<span class="na">		targetType</span><span class="pi">:</span> <span class="s">inline</span>
<span class="na">		script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="err">	</span><span class="s">		Set-StrictMode -Version Latest</span>
<span class="err">	</span><span class="s">		$ErrorActionPreference = 'Stop'</span>

<span class="err">	</span><span class="s">		#(Note: If you are on a 32-bit system, use .x86 instead).</span>
            <span class="s">winget install Microsoft.VCRedist.2015+.x64 </span>

            <span class="s"># Install latest stable</span>
            <span class="s">winget install --id 9NVTPZWRC6KQ --source msstore</span>
<span class="err">	</span><span class="s">		Install-Module -Name AzureDevOpsDscv3 -Scope CurrentUser -Force</span>
<span class="err">	</span><span class="s">		dsc --version</span>

<span class="err">-</span><span class="s"> task: PowerShell@2</span>
<span class="err">	</span><span class="s">displayName: Inject PAT and apply configuration</span>
<span class="err">	</span><span class="s">inputs:</span>
<span class="err">	</span><span class="s">	pwsh: true</span>
<span class="err">	</span><span class="s">	targetType: inline</span>
<span class="err">	</span><span class="s">	script: |</span>
<span class="err">	</span><span class="s">		Set-StrictMode -Version Latest</span>
<span class="err">	</span><span class="s">		$ErrorActionPreference = 'Stop'</span>

<span class="err">	</span><span class="s">		$configPath = "$(DscConfigPath)"</span>
<span class="err">	</span><span class="s">		$tempPath = Join-Path $env:Agent_TempDirectory 'ado.dsc.yaml'</span>

<span class="err">	</span><span class="s">		(Get-Content $configPath -Raw) -replace 'PAT-Token', $env:ADO_PAT | Set-Content $tempPath</span>

<span class="err">	</span><span class="s">		dsc -l debug config set --file $tempPath</span>
<span class="err">	</span><span class="s">env:</span>
<span class="err">	</span><span class="s">	ADO_PAT: $(ADO_PAT)</span>
</code></pre></div></div>

<h3 id="secure-the-pat">Secure the PAT</h3>

<p>Create a variable in your Azure DevOps pipeline or a variable group:</p>

<ul>
  <li>Name: <code class="language-plaintext highlighter-rouge">ADO_PAT</code></li>
  <li>Type: secret</li>
</ul>

<p>Keep the PAT scoped to the minimum permissions required to manage projects, users, and groups in your organization.</p>

<h2 id="run-the-pipeline">Run the Pipeline</h2>

<ol>
  <li>Commit the DSC config and pipeline YAML.</li>
  <li>Create a new pipeline from <code class="language-plaintext highlighter-rouge">pipelines/ado-dsc.yml</code>.</li>
  <li>Run the pipeline.</li>
</ol>

<p>The pipeline installs the required modules, injects the PAT into the config, and executes <code class="language-plaintext highlighter-rouge">dsc config set</code> to apply the desired state.</p>

<h2 id="real-world-tips">Real-World Tips</h2>

<ul>
  <li>Store multiple configs per environment and pass the file path as a pipeline parameter.</li>
  <li>Use a dedicated service account for the PAT to keep audit trails clean.</li>
  <li>Add a validation step that runs <code class="language-plaintext highlighter-rouge">dsc config get</code> (or <code class="language-plaintext highlighter-rouge">dsc resource list</code>) to verify expected resources.</li>
  <li>For production, add a manual approval gate between validation and apply.</li>
</ul>

<h2 id="troubleshooting">Troubleshooting</h2>

<ul>
  <li>If <code class="language-plaintext highlighter-rouge">dsc</code> is not found, confirm the <code class="language-plaintext highlighter-rouge">Microsoft.PowerShell.DSC</code> module is installed and available in the <code class="language-plaintext highlighter-rouge">pwsh</code> session.</li>
  <li>If the pipeline fails to create a project, check the PAT scope and the Azure DevOps organization name.</li>
  <li>For group assignment, verify <code class="language-plaintext highlighter-rouge">GroupOriginId</code> is the Entra ID object ID, not the display name.</li>
</ul>

<h2 id="closing">Closing</h2>

<p>This pattern lets you manage Azure DevOps configuration as code with minimal drift. Start small with one project and a few users, then expand to include repos, pipelines, and permissions in a controlled, repeatable way.</p>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="Azure" /><category term="DevOps" /><category term="DSC" /><category term="AzureDevOps" /><category term="Pipeline" /><summary type="html"><![CDATA[A real world example that uses AzureDevOpsDscv3 to create a project, add a group, and assign users with a repeatable Azure DevOps pipeline.]]></summary></entry><entry><title type="html">Mastering Azure Networking: Best Practices for Building Scalable, Secure Cloud Infrastructure</title><link href="https://mimachniak.github.io/sysopslife/2025/10/20/Mastering-Azure-Networking-Best-Practices-for-Building-Scalable-Secure-Cloud-Infrastructure/" rel="alternate" type="text/html" title="Mastering Azure Networking: Best Practices for Building Scalable, Secure Cloud Infrastructure" /><published>2025-10-20T00:00:00+02:00</published><updated>2025-10-20T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2025/10/20/Mastering-Azure%20Networking-Best-Practices-for-Building-Scalable-Secure-Cloud-Infrastructure</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2025/10/20/Mastering-Azure-Networking-Best-Practices-for-Building-Scalable-Secure-Cloud-Infrastructure/"><![CDATA[<h1 id="building-a-network-in-azure--from-zero-to-hero">Building a Network in Azure — From Zero to Hero</h1>

<p>Building a well-structured and secure network in Azure is one of the foundational steps toward a successful cloud deployment. Whether you’re migrating workloads, designing new environments, or improving existing infrastructure, Azure networking provides powerful tools to help you design for scalability, performance, and governance.</p>

<p>In this post, we’ll walk through the core principles and best practices for building a network in Azure — from initial planning to advanced connectivity options.</p>

<hr />

<h2 id="-start-with-the-azure-landing-zone-concept">🧭 Start with the Azure Landing Zone Concept</h2>

<p>Before diving into subnets and IP ranges, it’s crucial to understand the <strong>Azure Landing Zone</strong> model.</p>

<p>Microsoft defines an <strong>Azure Landing Zone</strong> as an environment that implements key design principles across <strong>eight design areas</strong> — governance, security, identity, networking, operations, management, and more. It ensures that your environment can scale and support multiple applications and workloads consistently.</p>

<h3 id="platform-vs-application-landing-zones">Platform vs Application Landing Zones</h3>

<p>Azure uses <strong>subscriptions</strong> to isolate and scale resources:</p>

<ul>
  <li><strong>Platform landing zones</strong> host shared services like networking, identity, and monitoring.</li>
  <li><strong>Application landing zones</strong> host workloads and apps.</li>
</ul>

<p>This separation allows for cleaner management boundaries, better security, and easier automation.</p>

<hr />

<h2 id="️-designing-your-network-topology">🕸️ Designing Your Network Topology</h2>

<p>One of the most common approaches for Azure networking is the <strong>hub-and-spoke topology</strong>.</p>

<h3 id="hub-and-spoke-model">Hub-and-Spoke Model</h3>

<p>In this model:</p>
<ul>
  <li>The <strong>hub</strong> is the central network that hosts shared components like firewalls, DNS, or VPN gateways.</li>
  <li>The <strong>spokes</strong> are individual VNets (Virtual Networks) that connect to the hub and host application workloads.</li>
</ul>

<p>This approach provides:</p>
<ul>
  <li>Centralized management of connectivity and security.</li>
  <li>Isolation between applications.</li>
  <li>Easier integration with on-premises networks.</li>
</ul>

<p>You can also use <strong>Azure Virtual Network Manager</strong> to create and manage:</p>
<ul>
  <li><strong>Hub-and-spoke topology</strong></li>
  <li><strong>Mesh topology</strong> (in preview)</li>
  <li><strong>Hybrid hub-spoke with direct spoke-to-spoke connectivity</strong></li>
</ul>

<p><img src="/assets/images/Azure/network/PL-Azure%20Networking-topology.png" alt="" /></p>

<hr />

<h2 id="-planning-your-network">🧱 Planning Your Network</h2>

<h3 id="mapping-on-premises-network-to-azure-network">Mapping on-premises network to Azure network</h3>

<p>Mapping VLANs to Azure networking depends on how and where you’re connecting Azure to your on-premises or extended network. 
Azure itself doesn’t use VLANs internally—it uses Virtual Networks (VNets) for segmentation—but VLANs still matter when you’re integrating Azure with your physical or hybrid infrastructure.</p>

<ul>
  <li>Use subnets within a VNet to logically segment traffic (similar to VLANs).</li>
  <li>Apply Network Security Groups (NSGs) or Azure Firewall rules for isolation.</li>
  <li>VLAN tagging or trunking is not available between Azure VMs or subnets.</li>
</ul>

<h3 id="naming-conventions">Naming Conventions</h3>

<p>A consistent naming convention is the foundation of an organized environment.<br />
Follow Microsoft’s <strong>Cloud Adoption Framework</strong> recommendations for naming standards:</p>
<blockquote>
  <table>
    <tbody>
      <tr>
        <td>[Cloud Adoption Framework</td>
        <td>Microsoft Learn](https://learn.microsoft.com/azure/cloud-adoption-framework/)</td>
      </tr>
    </tbody>
  </table>
</blockquote>

<p><img src="/assets/images/Azure/network/PL-Azure%20Networking-naming.drawio.png" alt="" /></p>

<h3 id="ip-addressing">IP Addressing</h3>

<p>When planning your VNets and subnets, avoid overlapping with your on-premises or partner networks.</p>

<h4 id="common-private-address-ranges">Common private address ranges:</h4>
<ul>
  <li><code class="language-plaintext highlighter-rouge">10.0.0.0/8</code></li>
  <li><code class="language-plaintext highlighter-rouge">172.16.0.0/12</code></li>
  <li><code class="language-plaintext highlighter-rouge">192.168.0.0/16</code></li>
  <li><code class="language-plaintext highlighter-rouge">100.64.0.0/10</code> (shared address space)</li>
</ul>

<p>Keep subnet masks small, such as <code class="language-plaintext highlighter-rouge">/28</code> or <code class="language-plaintext highlighter-rouge">/26</code>, to maintain flexibility and minimize wasted IPs.</p>

<h4 id="remember">Remember:</h4>
<p>Azure reserves certain IPs in every subnet:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">.0</code> — network address</li>
  <li><code class="language-plaintext highlighter-rouge">.1</code> — default gateway</li>
  <li><code class="language-plaintext highlighter-rouge">.2</code> and <code class="language-plaintext highlighter-rouge">.3</code> — Azure DNS mapping</li>
  <li><code class="language-plaintext highlighter-rouge">.255</code> — broadcast address</li>
</ul>

<h4 id="subnet-nameing-and-functionality-example">Subnet nameing and functionality example</h4>

<table>
  <thead>
    <tr>
      <th><strong>Subnet Name</strong></th>
      <th><strong>Description</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AzureFirewallSubnet</td>
      <td>Reserved for Azure Firewall</td>
    </tr>
    <tr>
      <td>GatewaySubnet</td>
      <td>Reserved for Azure Virtual Gateway</td>
    </tr>
    <tr>
      <td>AzureBastionSubnet</td>
      <td>Reserved for Azure Bastion</td>
    </tr>
    <tr>
      <td>ApplicationGatewaySubnet</td>
      <td>Custom – required delegation for application gateway</td>
    </tr>
    <tr>
      <td>ManagementSubnet</td>
      <td>Custom – for any management services like Update Managers, Scanning tools</td>
    </tr>
    <tr>
      <td>IdentitySubnet</td>
      <td>Custom – for identity services like Active Directory, PIM, PAM</td>
    </tr>
    <tr>
      <td>DmzSubnet</td>
      <td>Custom – for services that will be published to the Internet</td>
    </tr>
    <tr>
      <td>ApplicationSubnet</td>
      <td>Custom – for applications like CRM front, Reporting Services</td>
    </tr>
    <tr>
      <td>BackendSubnet</td>
      <td>Custom – backend API / Integration bus</td>
    </tr>
    <tr>
      <td>DatabaseSubnet</td>
      <td>Custom – for database services</td>
    </tr>
  </tbody>
</table>

<h4 id="subnet-ranges">Subnet ranges</h4>

<table>
  <thead>
    <tr>
      <th><strong>Subnet Name</strong></th>
      <th><strong>Network Mask</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AzureFirewallSubnet</td>
      <td>/26</td>
    </tr>
    <tr>
      <td>GatewaySubnet</td>
      <td>/26</td>
    </tr>
    <tr>
      <td>AzureBastionSubnet</td>
      <td>/26</td>
    </tr>
    <tr>
      <td>ApplicationGatewaySubnet</td>
      <td>/26</td>
    </tr>
    <tr>
      <td>ManagementSubnet</td>
      <td>/28</td>
    </tr>
    <tr>
      <td>IdentitySubnet</td>
      <td>/24</td>
    </tr>
    <tr>
      <td>DmzSubnet</td>
      <td>/24</td>
    </tr>
    <tr>
      <td>ApplicationSubnet</td>
      <td>/24</td>
    </tr>
    <tr>
      <td>BackendSubnet</td>
      <td>/24</td>
    </tr>
    <tr>
      <td>DatabaseSubnet</td>
      <td>/24</td>
    </tr>
  </tbody>
</table>

<h4 id="tool-for-to-help-design-your-subnets">Tool for to help design your subnets</h4>

<p>Visual Subnet Calculator</p>

<p>The Visual Subnet Calculator is a free, browser-based tool designed to assist network administrators and IT professionals in subnetting IPv4 networks.<br />
It offers a straightforward interface to quickly visualize and manage subnets without requiring advanced mathematical calculations.</p>

<blockquote>
  <p><a href="https://www.davidc.net/sites/default/subnets/subnets.html">Visual Subnet Calculator</a></p>
</blockquote>

<h2><img src="/assets/images/Azure/network/PL-Azure%20Networking-planner.png" alt="" /></h2>

<h2 id="-security-and-access">🔐 Security and Access</h2>

<p>Security starts with <strong>Network Security Groups (NSGs)</strong> is easier to unblock traffic, then setup block rules on production environment.<br />
It’s a best practice to:</p>
<ul>
  <li>Create <strong>custom rules</strong>.</li>
  <li>Block all inbound and outbound traffic by default.</li>
  <li>Explicitly allow only what’s required.</li>
</ul>

<p><img src="/assets/images/Azure/network/PL-Azure%20Networking-NSG.png" alt="" /></p>

<p><img src="/assets/images/Azure/network/PL-Azure%20Networking-Private-Subnet.png" alt="" /></p>

<hr />

<h2 id="-private-connectivity">🔗 Private Connectivity</h2>

<p>When connecting Azure services privately:</p>
<ul>
  <li>Use <strong>Private Link</strong> for secure, private access to Azure services like Storage, SQL, and Key Vault.</li>
  <li>Use <strong>Private DNS Zones</strong> to manage DNS resolution for private endpoints.
    <ul>
      <li>A single Private DNS Zone can be linked to multiple VNets.</li>
      <li>No need to deploy a separate DNS resolver for every network.</li>
    </ul>
  </li>
</ul>

<p><strong>Microsoft DNS</strong>: Azure IP address <strong>168.63.129.16</strong> is a virtual public IP address that facilitates communication channels to Azure platform resources. Customers can define any address space for their private virtual network in Azure. Therefore, the Azure platform resources must be presented as a unique public IP address</p>

<hr />

<h2 id="-monitoring-your-network">🔍 Monitoring Your Network</h2>

<p>Don’t forget about visibility and diagnostics.<br />
Use <strong>Network Watcher</strong> to:</p>
<ul>
  <li>Monitor traffic flows.</li>
  <li>Diagnose connectivity issues.</li>
  <li>Capture packets and analyze performance.</li>
</ul>

<hr />

<h2 id="-is-one-subscription-enough">🧰 Is One Subscription Enough?</h2>

<p>For small environments, a single Azure subscription can be sufficient.<br />
However, for larger organizations, <strong>multiple subscriptions</strong> provide better scalability, governance, and isolation between workloads or teams.</p>

<p>Example of building and envoling network in azure for organization</p>

<h3 id="example-for-organization-with-one-azure-subscription">Example for organization with one Azure subscription</h3>

<h4 id="example-1">Example 1</h4>
<ul>
  <li>One Azure subscription</li>
  <li>One Azure Virtual Network</li>
  <li>Azure Virtual Network with subntes (segmentation)</li>
  <li>Network Secuirty groups for all subnetes with DenyRules for Inbound / Outbound</li>
  <li>Azure Virtual Gateway for Site to Site VPN connection</li>
  <li>Azure Nat Gateway for NAT outbound traffic</li>
</ul>

<p><img src="/assets/images/Azure/network//PL-Azure%20Networking%20from%20Zero%20to%20Hero-V1.drawio.png" alt="" /></p>

<h4 id="example-2">Example 2</h4>
<ul>
  <li>One Azure subscription</li>
  <li>One Azure Virtual Network</li>
  <li>Azure Virtual Network with subntes (segmentation)</li>
  <li>Network Secuirty groups for all subnetes with DenyRules for Inbound / Outbound</li>
  <li>Azure Virtual Gateway for Site to Site VPN connection</li>
  <li>Azure Nat Gateway for NAT outbound traffic</li>
  <li>Azure Private DNS zones dedicated for Azure SQL services</li>
  <li>Azure privet link for Azure SQL server</li>
  <li>Azure SQL server without any Internet access</li>
</ul>

<p><img src="/assets/images/Azure/network//PL-Azure%20Networking%20from%20Zero%20to%20Hero-V2.drawio.png" alt="" /></p>

<h4 id="example-2-1">Example 2</h4>
<ul>
  <li>One Azure subscription</li>
  <li>One Azure Virtual Network</li>
  <li>Azure Virtual Network with subntes (segmentation)</li>
  <li>Network Secuirty groups for all subnetes with DenyRules for Inbound / Outbound</li>
  <li>Azure Virtual Gateway for Site to Site VPN connection</li>
  <li>Azure Nat Gateway for NAT outbound traffic</li>
  <li>Azure Private DNS zones dedicated for Azure SQL services</li>
  <li>Azure privet link for Azure SQL server</li>
  <li>Azure SQL server without any Internet access</li>
  <li>Azure Firewall that inspect all traffic between subnets</li>
  <li>Azure user definie route to pass traffic between subnets over Azure firewall</li>
</ul>

<p><img src="/assets/images/Azure/network//PL-Azure%20Networking%20from%20Zero%20to%20Hero-V3.drawio.png" alt="" /></p>

<h3 id="example-for-organization-with-multiple-azure-subscription-hub">Example for organization with multiple Azure subscription (Hub)</h3>

<h4 id="example-1-1">Example 1</h4>
<ul>
  <li>3 Azure subscription</li>
  <li>3 Azure Virtual Network</li>
  <li>Azure Virtual Network with subntes (segmentation)</li>
  <li>Network Secuirty groups for all subnetes with DenyRules for Inbound / Outbound</li>
  <li>Azure Virtual Gateway for Site to Site VPN connection</li>
  <li>Azure Nat Gateway for NAT outbound traffic</li>
  <li>Azure Private DNS zones dedicated for Azure SQL services</li>
  <li>Azure privet link for Azure SQL server</li>
</ul>

<p><img src="/assets/images/Azure/network//PL-Azure%20Networking%20from%20Zero%20to%20Hero-V4.drawio.png" alt="" /></p>

<ul>
  <li>3 Azure subscription</li>
  <li>3 Azure Virtual Network</li>
  <li>Azure Virtual Network with subntes (segmentation)</li>
  <li>Network Secuirty groups for all subnetes with DenyRules for Inbound / Outbound</li>
  <li>Azure Virtual Gateway for Site to Site VPN connection</li>
  <li>Azure Nat Gateway for NAT outbound traffic</li>
  <li>Azure Private DNS zones dedicated for Azure SQL services</li>
  <li>Azure privet link for Azure SQL server</li>
  <li>Azure Network Manager</li>
</ul>

<p><img src="/assets/images/Azure/network//PL-Azure%20Networking%20from%20Zero%20to%20Hero-V5.drawio.png" alt="" /></p>

<hr />

<h2 id="️-tips-and-best-practices">⚙️ Tips and Best Practices</h2>

<ul>
  <li>Create good naming convention</li>
  <li>Keep it simple</li>
  <li>Subnet with small mask like 28 / 26</li>
  <li>One Virtual Network per Azure Subscription per region</li>
  <li>Use NAT gateway</li>
  <li>Avoid overlap network spaces with you on-premises environment</li>
  <li>Use Hub spoken network if it’s needed</li>
  <li>Not all resources need to have privet access</li>
  <li>DNS resolving is important – all azure resources work over HTTPS</li>
  <li>Good description of routing tables</li>
  <li>Private DNS Zones can be linked to multiple Vnet don’t need to user DNS resolver</li>
  <li>NSG create custom rule block all Inboud / Outboud traffic</li>
  <li>For internet access, use <strong>NAT Gateways</strong> instead of assigning public IPs directly to VMs.</li>
</ul>

<hr />

<h2 id="-summary">🧩 Summary</h2>

<p>Building a robust Azure network requires thoughtful design and attention to detail.<br />
Start with a solid landing zone, plan your IPs and subnets carefully, use secure connectivity methods, and monitor everything.</p>

<p>When done right, your Azure network becomes the backbone for all your cloud workloads — secure, scalable, and future-proof.</p>

<hr />

<p><em>Author: Michał Machniak</em><br />
<em>System Administrator / Cloud Architect / DevOps</em><br />
<a href="https://mmachniak.net">mmachniak.net</a></p>

<blockquote>
  <p>“Automate everything you can — but plan your network manually first.”</p>
</blockquote>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="Azure" /><category term="Network" /><category term="Security" /><summary type="html"><![CDATA[Discover how to build a secure and scalable Azure network from the ground up. Learn how to start with a simple network for a small company and seamlessly extend it into an enterprise-grade architecture. Explore best practices for Azure Landing Zones, hub-and-spoke topology, private connectivity, DNS configuration, and network security to design flexible, future-ready cloud infrastructure in Microsoft Azure.]]></summary></entry><entry><title type="html">Delegate for Azure ARC least-privilege access for hybrid servers - Custom role</title><link href="https://mimachniak.github.io/sysopslife/2025/09/22/Azure-Arc-Custom-Role-Virtual-Machine-Operator/" rel="alternate" type="text/html" title="Delegate for Azure ARC least-privilege access for hybrid servers - Custom role" /><published>2025-09-22T00:00:00+02:00</published><updated>2025-09-22T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2025/09/22/Azure-Arc-Custom-Role-Virtual-Machine-Operator</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2025/09/22/Azure-Arc-Custom-Role-Virtual-Machine-Operator/"><![CDATA[<h2 id="creating-a-custom-azure-role-for-azure-arc-hybrid-compute">Creating a Custom Azure Role for Azure Arc Hybrid Compute</h2>

<p>When working with Azure Arc–enabled servers, you often need to control access in a way that doesn’t fit the built-in roles provided by Azure. While roles like Reader, Contributor, or Azure Connected Machine Onboarding cover many scenarios, they might either grant too many permissions or not enough.</p>

<p>That’s where custom roles come in. By defining a custom role, you can tailor access so that users, groups, or service principals only get the permissions they actually need — nothing more, nothing less.</p>

<h2 id="why-create-a-custom-role-for-azure-arc">Why Create a Custom Role for Azure Arc?</h2>

<p>Some common scenarios include:</p>

<ul>
  <li>
    <p>Delegated administration: Allowing specific teams to manage extensions or policies on Arc-enabled machines, without giving them full subscription-wide rights.</p>
  </li>
  <li>
    <p>Security hardening: Enforcing least-privilege access so that hybrid server operators can perform their job without unnecessary permissions.</p>
  </li>
  <li>
    <p>Operational separation: Letting one team onboard servers, while another team manages monitoring or updates.</p>
  </li>
</ul>

<p>For example, you may want a role that only allows installation and management of VM extensions (like Azure Monitor Agent or Defender for Cloud extensions) on Arc servers — but nothing else.</p>

<h2 id="step-1-identify-the-required-permissions">Step 1: Identify the Required Permissions</h2>

<p>Azure roles are defined as sets of Actions and NotActions. For Azure Arc hybrid compute, common permissions include:</p>

<ul>
  <li>
    <p>Microsoft.HybridCompute/machines/read – View Arc machines</p>
  </li>
  <li>
    <p>Microsoft.HybridCompute/machines/extensions/* – Manage extensions</p>
  </li>
  <li>
    <p>Microsoft.HybridCompute/machines/write – Update properties (if needed)</p>
  </li>
</ul>

<p>You can explore available permissions with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
az provider operation show <span class="nt">--namespace</span> Microsoft.HybridCompute

</code></pre></div></div>

<h2 id="step-2-define-the-custom-role-json">Step 2: Define the Custom Role JSON</h2>

<p>Save the following as ArcOperator.json:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[Custom] Azure Arc Operator"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"IsCustom"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"View, update patch managment for hybride VM."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Actions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                    </span><span class="s2">"*/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/operations/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/osType/agentVersions/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/osType/agentVersions/latest/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/installPatches/action"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/listAccessDetails/action"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/UpgradeExtensions/action"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/assessPatches/action"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/addExtensions/action"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/patchInstallationResults/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/patchInstallationResults/softwarePatches/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/extensions/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/extensions/write"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/extensions/delete"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/patchAssessmentResults/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/patchAssessmentResults/softwarePatches/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/runcommands/read"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/runcommands/write"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/runcommands/delete"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"Microsoft.HybridCompute/machines/write"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"NotActions"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
  </span><span class="nl">"AssignableScopes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"/subscriptions/&lt;SUBSCRIPTION_ID&gt;"</span><span class="w"> </span><span class="err">#</span><span class="w"> </span><span class="err">Can</span><span class="w"> </span><span class="err">be</span><span class="w"> </span><span class="err">Managment</span><span class="w"> </span><span class="err">group</span><span class="w"> </span><span class="err">/</span><span class="w"> </span><span class="err">Resource</span><span class="w"> </span><span class="err">group</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="step-3-create-the-role-in-azure">Step 3: Create the Role in Azure</h2>

<p>Use the Azure CLI to create the role:</p>

<p>az role definition create –role-definition ./ArcOperator.json</p>

<p>To verify:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
az role definition list <span class="nt">--name</span> <span class="s2">"[Custom] Azure Arc Operator"</span>

</code></pre></div></div>

<h2 id="step-4-assign-the-role">Step 4: Assign the Role</h2>

<p>Assign the role to a user, group, or managed identity:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az role assignment create <span class="se">\</span>
  <span class="nt">--assignee</span> &lt;USER_OR_SP_OBJECT_ID&gt; <span class="se">\</span>
  <span class="nt">--role</span> <span class="s2">"Arc Extension Operator"</span> <span class="se">\</span>
  <span class="nt">--scope</span> /subscriptions/&lt;SUBSCRIPTION_ID&gt;/resourceGroups/&lt;RESOURCE_GROUP&gt;
</code></pre></div></div>

<p>This ensures that the principal can only manage Arc extensions within the defined scope.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Azure Arc custom roles let you implement least-privilege access for hybrid servers. By carefully crafting role definitions, you can give teams exactly the permissions they need — no more, no less.</p>

<p>This approach improves security, simplifies operations, and ensures compliance with enterprise governance requirements.</p>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="Azure" /><category term="Shell" /><category term="VirtualMachine" /><summary type="html"><![CDATA[This blog post explains why organizations using Azure Arc–enabled servers should implement least-privilege access through custom roles. It walks through real-world scenarios where built-in Azure roles are either too broad or too restrictive and shows how to define a custom Azure role tailored for Arc hybrid compute,]]></summary></entry><entry><title type="html">Using the Microsoft Azure Bicep Graph Extension to Create Security Groups and Assign Roles in an Azure Landing Zone</title><link href="https://mimachniak.github.io/sysopslife/2025/08/11/Bicep-Graph-Extension-ALZ-RBAC/" rel="alternate" type="text/html" title="Using the Microsoft Azure Bicep Graph Extension to Create Security Groups and Assign Roles in an Azure Landing Zone" /><published>2025-08-11T00:00:00+02:00</published><updated>2025-08-11T00:00:00+02:00</updated><id>https://mimachniak.github.io/sysopslife/2025/08/11/Bicep-Graph-Extension-ALZ-RBAC</id><content type="html" xml:base="https://mimachniak.github.io/sysopslife/2025/08/11/Bicep-Graph-Extension-ALZ-RBAC/"><![CDATA[<h2 id="description">Description</h2>

<p>In large, multi-subscription environments, such as Azure Landing Zones, managing identity and access at scale is a constant challenge. Security groups are a key component of Azure RBAC (Role-Based Access Control), enabling centralized control of permissions for teams and workloads.
While you can configure them manually in the Azure Portal, Infrastructure as Code (IaC) ensures consistency, repeatability, and compliance.</p>

<p>With Bicep and the Microsoft Graph extension, we can define security groups and assign Azure roles programmatically—integrating identity management directly into our landing zone deployment workflows</p>

<h2 id="why-use-the-microsoft-graph-extension-in-bicep">Why Use the Microsoft Graph Extension in Bicep?</h2>
<p>By default, Bicep focuses on Azure Resource Manager (ARM) resources. However, security groups in Entra ID (formerly Azure AD) are not ARM resources—they are managed through Microsoft Graph. The Bicep Graph extension bridges this gap, enabling you to:</p>

<ul>
  <li>
    <p>Create Azure AD / Entra ID security groups.</p>
  </li>
  <li>
    <p>Assign users or service principals to those groups.</p>
  </li>
  <li>
    <p>Bind those groups to Azure roles within subscriptions or resource groups.</p>
  </li>
  <li>
    <p>This allows a single Bicep deployment to set up both your Azure resources and the associated identity access controls.</p>
  </li>
</ul>

<h2 id="example-of-code--creating-a-security-group-and-assigning-a-role">Example of code : Creating a Security Group and Assigning a Role</h2>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">
</span><span class="n">/</span><span class="o">*</span><span class="w">
</span><span class="n">SUMMARY:</span><span class="w"> </span><span class="nx">The</span><span class="w"> </span><span class="nx">Management</span><span class="w"> </span><span class="nx">Groups</span><span class="w"> </span><span class="nx">module</span><span class="w"> </span><span class="nx">deploys</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="w"> </span><span class="nx">in</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="nx">customer</span><span class="s1">'s tenant under the '</span><span class="nx">Tenant</span><span class="w"> </span><span class="nx">Root</span><span class="w"> </span><span class="nx">Group</span><span class="s1">'.
DESCRIPTION: Management Group hierarchy is created through a tenant-scoped Azure Resource Manager (ARM) deployment.  The hierarchy is:
  * Tenant Root Group
      * Organization
        ** Platform
        ** DEV
        ** STAGE
        ** PROD
      * Default - new onborded subscriptions
AUTHOR/S: Michał Machniak
VERSION: 1.0
Logs: Get-AzTenantDeployment -id /providers/Microsoft.Resources/deployments/azl | Get-AzTenantDeploymentOperation
Logs: Get-AzTenantDeployment -id /providers/Microsoft.Resources/deployments/azl-rbac | Get-AzTenantDeploymentOperation
*/

targetScope = '</span><span class="nx">tenant</span><span class="s1">'

extension '</span><span class="nx">br:mcr.microsoft.com/bicep/extensions/microsoftgraph/beta:1.0.0</span><span class="s1">' // Load beta extension
extension '</span><span class="nx">br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:1.0.0</span><span class="s1">' // Load v1.0 extension

///////////////////////// PARAMETERS /////////////////////////

param par_list_secuirty_groups_name array = [
  {
    name: '</span><span class="nx">GAL-AAD-MG-Organization-Org-Reader</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">read</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Reader</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGXYZOrgOrgReader</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">Organization</span><span class="s1">' // Management Group ID to assign the group to
  }
  {
    name: '</span><span class="nx">GAL-AAD-MG-Organization-Contributor</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">contribute</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Contributor</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGXYZOrgOrgContributor</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">Organization</span><span class="s1">' // Management Group ID to assign the group to
  }
  {
    name:'</span><span class="nx">GAL-AAD-MG-Organization-Owner</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">manage</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Owner</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGXYZOrgOrgOwner</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">Organization</span><span class="s1">' // Management Group ID to assign the group to
  }
  // Dev Groups
  {
    name: '</span><span class="nx">GAL-AAD-MG-DEV-Reader</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">read</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Reader</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGDEVReader</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">DEV</span><span class="s1">' // Management Group ID to assign the group to
  }
  {
    name: '</span><span class="nx">GAL-AAD-MG-DEV-Contributor</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">contribute</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Contributor</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGDEVContributor</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">DEV</span><span class="s1">' // Management Group ID to assign the group to

  }
  {
    name:'</span><span class="nx">GAL-AAD-MG-DEV-Owner</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">manage</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Owner</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGDEVOwner</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">DEV</span><span class="s1">' // Management Group ID to assign the group to
  }
  // Prod groups
  {
    name: '</span><span class="nx">GAL-AAD-MG-PROD-Reader</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">read</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Reader</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGPRODReader</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">PROD</span><span class="s1">' // Management Group ID to assign the group to
  }
  {
    name: '</span><span class="nx">GAL-AAD-MG-PROD-Contributor</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">contribute</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Contributor</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGPRODContributor</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">PROD</span><span class="s1">' // Management Group ID to assign the group to

  }
  {
    name:'</span><span class="nx">GAL-AAD-MG-PROD-Owner</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">manage</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Owner</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGPRODOwner</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">PROD</span><span class="s1">' // Management Group ID to assign the group to
  }
    // Prod Default
  {
    name: '</span><span class="nx">GAL-AAD-MG-Default-Reader</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">read</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Reader</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGDefualtReader</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">Default</span><span class="s1">' // Management Group ID to assign the group to
  }
  {
    name: '</span><span class="nx">GAL-AAD-MG-Default-Contributor</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">contribute</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Contributor</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMDefualtContributor</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">Default</span><span class="s1">' // Management Group ID to assign the group to
  }
  {
    name:'</span><span class="nx">GAL-AAD-MG-Default-Owner</span><span class="s1">'
    description: '</span><span class="nx">Security</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">permissions</span><span class="w"> </span><span class="nx">to</span><span class="w"> </span><span class="nx">manage</span><span class="w"> </span><span class="nx">the</span><span class="w"> </span><span class="nx">management</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">hierarchy</span><span class="s1">'
    role: '</span><span class="nx">Owner</span><span class="s1">'
    securityEnabled: true
    mailEnabled: false // Need to be false is not supported by v1.0 and beta
    uniqueName: '</span><span class="nx">GALAADMGDefualtOwner</span><span class="s1">' // No whitspaces allowed
    MangmentGroupId: '</span><span class="nx">Default</span><span class="s1">' // Management Group ID to assign the group to
  }

]


resource res_secuirty_group_alz '</span><span class="nx">Microsoft.Graph/groups</span><span class="err">@</span><span class="nx">beta</span><span class="s1">' = [for sec_group in par_list_secuirty_groups_name: {
  displayName: sec_group.name
  description: sec_group.description
  securityEnabled: true
  mailEnabled: false // Need to be false is not supported by v1.0 and beta
  mailNickname: toLower('</span><span class="nv">${sec_group.name}</span><span class="s1">')
  owners: {
    relationships: []
  }
  members: {
    relationships: []
  }
  uniqueName: sec_group.uniqueName  // No whitspaces allowed

}]



// Assign security groups to management groups - need delay against the creation of the security groups

module mod_mg_role_permissions '</span><span class="o">.</span><span class="nf">modules</span><span class="nx">/RoleAssigment/managmentGroups.bicep</span><span class="s1">' = [for i in range(0, length(par_list_secuirty_groups_name)) : {
  name: guid(par_list_secuirty_groups_name[i].name)
  scope: managementGroup('</span><span class="nv">${par_list_secuirty_groups_name[i].MangmentGroupId}</span><span class="s1">')
  params: {
    principalId: res_secuirty_group_alz[i].id
    roleDefinitionIdOrName: par_list_secuirty_groups_name[i].role
    managementGroupId: par_list_secuirty_groups_name[i].MangmentGroupId
  }
  dependsOn: [
    res_secuirty_group_alz
  ]
}]

output out_groups_id array = [for i in range(0, length(par_list_secuirty_groups_name)): res_secuirty_group_alz[i].id]

</span></code></pre></div></div>

<h2 id="example-deployment-command">Example Deployment Command:</h2>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">
</span><span class="n">New-AzTenantDeployment</span><span class="w"> </span><span class="nt">-Location</span><span class="w"> </span><span class="s2">"North Europe"</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"AAD-v2"</span><span class="w"> </span><span class="nt">-TemplateFile</span><span class="w"> </span><span class="o">.</span><span class="nx">\main-aad-loop.bicep</span><span class="w">

</span><span class="n">az</span><span class="w"> </span><span class="nx">deployment</span><span class="w"> </span><span class="nx">sub</span><span class="w"> </span><span class="nx">tenant</span><span class="w"> </span><span class="nt">--location</span><span class="w"> </span><span class="nx">northeurope</span><span class="w"> </span><span class="nt">--template-file</span><span class="w"> </span><span class="o">.</span><span class="nx">\main-aad-loop.bicep</span><span class="w">

</span></code></pre></div></div>

<h2 id="link-to-repo-with-code">Link to repo with code:</h2>

<p>You can find code example in my GitHub repository: <a href="https://github.com/mimachniak/sysopslife-scripts/tree/master/Azure/bicep/Microsoft-Graph-code">Github Bicep code example</a></p>

<h2 id="know-issue">Know issue</h2>

<p>When you create security groups in EntraID, it will take time for Graph API to procced chnages, so you can see that assigne permissions will faild because object wasn not found in Entra I, so this will required to rerun bicep as EntraID need some time.</p>

<h2 id="conclusion">Conclusion</h2>
<p>By using the Microsoft Azure Bicep Graph extension, you can extend your IaC workflows beyond Azure resources, directly into identity and access management. This approach helps ensure that your Azure Landing Zone deployments are secure, consistent, and repeatable—with all permissions configured as code.</p>]]></content><author><name>Michal Machniak</name><email>mmachniak@outlook.com</email></author><category term="Azure" /><category term="Bicep" /><category term="IaaC" /><category term="EntraID" /><summary type="html"><![CDATA[In large, multi-subscription environments, such as Azure Landing Zones, managing identity and access at scale is a constant challenge. Security groups are a key component of Azure RBAC (Role-Based Access Control), enabling centralized control of permissions for teams and workloads. While you can configure them manually in the Azure Portal, Infrastructure as Code (IaC) ensures consistency, repeatability, and compliance. With Bicep and the Microsoft Graph extension, we can define security groups and assign Azure roles programmatically—integrating identity management directly into our landing zone deployment workflows.]]></summary></entry></feed>