Back to Devoured - August 03, 2026
DEVOURED
Deploy Enterprise PowerShell Modules Using Azure Artifacts

Deploy Enterprise PowerShell Modules Using Azure Artifacts

DevOps Adam the Automator
You can distribute enterprise PowerShell modules securely by using Azure Artifacts as a private feed, managed via CI/CD pipelines and JEA.
What: Adam Bertram details a pipeline using Azure Artifacts for private PowerShell module hosting, incorporating GitVersion for semantic versioning, Azure Key Vault for code signing, and JEA to restrict privileged access.
Why it matters: Standardizing module distribution is critical for avoiding 'DLL hell' equivalent issues in PowerShell environments as automation code scales.
Takeaway: Register your private feed using `Register-PSResourceRepository` and enforce code signing using AzureSignTool to ensure supply chain integrity.
Deep dive
  • Feed Hosting: Use Azure Artifacts NuGet-based feeds for internal PowerShell modules.
  • Versioning: Automate semantic versioning using GitVersion with a fetchDepth: 0 setting.
  • Security: Sign modules with Authenticode via AzureSignTool and Azure Key Vault.
  • Privilege Control: Use JEA (Just Enough Administration) to limit function execution to specific roles.
Decoder
  • JEA: A security technology that allows users to perform administrative tasks with standard permissions by restricting the commands they can execute within a PowerShell session.
  • PSResourceGet: The modern replacement for PowerShellGet, used to manage repositories and install modules.
Original article

You have a PowerShell module that started as a helpful script and then became business-critical. Now every admin workstation, build agent, and jump box needs the same version at the same time.

Copying module folders to file shares works for a while. But file shares do not give you clean version history, feed permissions, pipeline publishing, or a nice Install-PSResource experience. Azure Artifacts does.

In this tutorial, you will build a private NuGet feed for enterprise PowerShell modules, publish modules from Azure DevOps, stamp them with GitVersion, sign script files using an Azure Key Vault-backed signing workflow, and expose the module through Just-Enough-Administration (JEA).

Prerequisites

To follow along, you will need the following:

  • An Azure DevOps organization and project.
  • Permission to create or administer an Azure Artifacts feed.
  • A PowerShell module with a .psd1 manifest.
  • PowerShell 7.x with the Microsoft.PowerShell.PSResourceGet module available.
  • An Azure Key Vault certificate or signing process approved by your security team.
  • A Windows host if you plan to configure JEA endpoints.

This tutorial uses placeholders such as <ORGANIZATION_NAME>, <PROJECT_NAME>, and <FEED_NAME>. Replace those values with your environment names before running the commands.

Creating an Azure Artifacts Feed for PowerShell Modules

Azure Artifacts feeds can store NuGet packages, and PowerShell modules published with PSResourceGet use a NuGet-based repository model. That combination makes Azure Artifacts a solid private PowerShell repository for internal modules.

In Azure DevOps, open Artifacts, create a new feed, and choose the visibility you want. For a central enterprise module feed, start with project-scoped permissions unless you intentionally need the feed available across the entire organization.

After creating the feed, open Connect to feed, choose NuGet.exe, and copy the v3 feed URL.

Registering the Feed with PSResourceGet

PSResourceGet replaces the older PowerShellGet v2 publishing and installation cmdlets with commands such as Register-PSResourceRepository, Publish-PSResource, and Install-PSResource.

On an administrator workstation, register the Azure Artifacts feed as a trusted repository. If you have already configured a compatible NuGet credential provider or your shell is authenticated through your enterprise process, the repository registration is only the feed metadata.

Register-PSResourceRepository -Name 'EnterpriseModules' `
    -Uri 'https://pkgs.dev.azure.com/<ORGANIZATION_NAME>/<PROJECT_NAME>/_packaging/<FEED_NAME>/nuget/v3/index.json' `
    -Trusted

If your organization stores feed credentials in a vault supported by PSResourceGet, register credential information with the repository:

$credentialInfo = [Microsoft.PowerShell.PSResourceGet.UtilClasses.PSCredentialInfo]::new(
    'SecretStore',
    'EnterpriseModulesFeed'
)

Register-PSResourceRepository -Name 'EnterpriseModules' `
    -Uri 'https://pkgs.dev.azure.com/<ORGANIZATION_NAME>/<PROJECT_NAME>/_packaging/<FEED_NAME>/nuget/v3/index.json' `
    -Trusted `
    -CredentialInfo $credentialInfo `
    -Force

Verify the repository registration:

Get-PSResourceRepository -Name 'EnterpriseModules'

Installing from the feed is now a normal PowerShell resource operation:

Install-PSResource -Name 'Contoso.Operations' -Repository 'EnterpriseModules'

Preparing the Module Manifest for CI/CD

Your module manifest is the contract between your code and the package consumers. At minimum, confirm the manifest has a module version, root module, author, description, exported functions, and compatible PowerShell edition details.

Before packaging, validate the manifest locally:

Test-ModuleManifest -Path .\Contoso.Operations\Contoso.Operations.psd1

Adding Semantic Versioning with GitVersion

GitVersion calculates semantic versions from your Git history. In Azure DevOps, GitVersion’s current guidance calls out an important setting: disable shallow fetch by setting fetchDepth: 0, or GitVersion can fail because the pipeline did not clone enough history.

Add a GitVersion.yml file to the repository root:

mode: ContinuousDelivery
branches:
  main:
    regex: ^main$
    increment: Patch
  pull-request:
    regex: ^(pull|pull-requests|pr)[/-]
    increment: Inherit

Signing Module Scripts with Azure Key Vault

Code signing gives consumers confidence that the module files came from your pipeline and were not modified after publishing. For enterprise modules, sign the .ps1, .psm1, and other script files before calling Publish-PSResource.

One practical option is AzureSignTool, which signs Authenticode content using a certificate in Azure Key Vault.

dotnet tool install --global --version 7.0.0 AzureSignTool

A signing step can sign all PowerShell script files in the module folder:

$files = Get-ChildItem -Path '.\Contoso.Operations' -Include '*.ps1','*.psm1' -Recurse

foreach ($file in $files) {
  AzureSignTool sign -kvu 'https://<KEY_VAULT_NAME>.vault.azure.net/' `
  -kvc '<CERT_NAME>' -kvm `
  -tr 'http://timestamp.digicert.com' -td sha384 `
  -fd sha384 -v `
  $file.FullName 
  }

Publishing from Azure DevOps

Create an azure-pipelines.yml file to wire the feed, versioning, signing, and publishing together:

trigger:
  - main

pool:
  vmImage: 'windows-latest'

steps:
  - checkout: self
    fetchDepth: 0

  - task: GitVersion@3
    displayName: 'Install GitVersion'
    inputs:
      versionSpec: '6.7.x'

  - task: GitVersion@3
    displayName: 'Calculate GitVersion'
    name: gitversion

  - task: NuGetAuthenticate@1

  - pwsh: |
      $manifestPath = '.\Contoso.Operations\Contoso.Operations.psd1'
      Update-ModuleManifest -Path $manifestPath -ModuleVersion '$(GitVersion.SemVer)'
      Test-ModuleManifest -Path $manifestPath
    displayName: 'Stamp and validate module manifest'

  - pwsh: |
      # (Signing logic here)
    displayName: 'Sign module files'

  - pwsh: |
      $pat = $env:SYSTEM_ACCESSTOKEN
      $securePat = ConvertTo-SecureString $pat -AsPlainText -Force
      $credential = [pscredential]::new('AzureDevOps', $securePat)

      Register-PSResourceRepository -Name 'EnterpriseModules' `
        -Uri 'https://pkgs.dev.azure.com/<ORGANIZATION_NAME>/<PROJECT_NAME>/_packaging/<FEED_NAME>/nuget/v3/index.json' `
        -Trusted `
        -Force

      Publish-PSResource -Path '.\Contoso.Operations' `
        -Repository 'EnterpriseModules' `
        -Credential $credential `
        -ApiKey 'AzureArtifacts'
    displayName: 'Publish module to Azure Artifacts'
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)

Installing the Module on Servers

Once the pipeline publishes the package, servers and admin workstations install the module from the private feed:

Register-PSResourceRepository -Name 'EnterpriseModules' `
  -Uri 'https://pkgs.dev.azure.com/<ORGANIZATION_NAME>/<PROJECT_NAME>/_packaging/<FEED_NAME>/nuget/v3/index.json' `
  -Trusted

Install-PSResource -Name 'Contoso.Operations' -Repository 'EnterpriseModules' -Scope AllUsers

Restricting Module Actions with JEA

Publishing a module securely is only half the story. You still need to control who can run privileged functions from that module. JEA lets you expose a constrained PowerShell endpoint with only the commands a role needs.

Create a role capability file:

$roleParameters = @{
  Path = 'C:\Program Files\WindowsPowerShell\Modules\Contoso.Operations\RoleCapabilities\ServiceOperator.psrc'
  Author = 'Contoso IT'
  CompanyName = 'Contoso'
  Description = 'Allows service operators to restart approved Contoso services.'
  ModulesToImport = 'Contoso.Operations'
  VisibleFunctions = 'Restart-ContosoService'
  }

New-PSRoleCapabilityFile @roleParameters

Next, create and register a session configuration:

$roleDefinitions = @{
  'CONTOSO\Service Operators' = @{ RoleCapabilities = 'ServiceOperator' }
  }

New-PSSessionConfigurationFile -Path 'C:\JEA\ContosoService.pssc' `
  -SessionType RestrictedRemoteServer `
  -RunAsVirtualAccount `
  -RoleDefinitions $roleDefinitions

Register-PSSessionConfiguration -Name 'ContosoService' `
  -Path 'C:\JEA\ContosoService.pssc' `
  -Force

Operational Tips for Enterprise Module Distribution

  • Publish only from CI/CD. Do not let humans publish production module versions from workstations.
  • Treat the module manifest as a build artifact. Stamp it, validate it, and publish that exact folder.
  • Use semantic versions and never overwrite released versions.
  • Require valid Authenticode signatures before publishing.
  • Give feed readers broad access, but feed publishers limited access.
  • Separate development, test, and production feeds if release approvals matter.
  • Add JEA endpoints for privileged functions instead of granting broad local administrator rights.

Conclusion

You built a private PowerShell module distribution flow using Azure Artifacts and PSResourceGet. You created a NuGet feed, registered it as a PowerShell repository, published modules through Azure DevOps, stamped versions with GitVersion, signed files with an Azure Key Vault-backed signing workflow, and restricted privileged operations with JEA.

Explore all 67 articles in today's digest

Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi