Title image

The PowerShell Gallery is full of useful modules shared by community members, but the nature of this community content makes the gallery inherently untrusted. In this post I will have a look at how you can improve your Supply Chain Security and availability by utilizing Microsoft Artifact Registry and building your own private PSGallery.

$PSGallery = -not $secure

Aqua published an article outlining some of the issues with the gallery and compared it to other package managers, you can read it in their blog. Among these issues is the risk of typosquatting in the module names, similar to GitHub Actions name. See orca security’s blog about typosquatting in GitHub Actions.

Even the default psresource repository in pwsh is marked untrusted by default.

Get-PSResourceRepository

Name      Uri                                      Trusted Priority IsAllowedByPolicy
----      ---                                      ------- -------- -----------------
PSGallery https://www.powershellgallery.com/api/v2 False   50       True

Availability of PSGallery vs ACR

The PSGallery is not backed by any SLA and is known to experience issues.

By building our own PSGallery in an Azure Container Registry it garantees at least 99.9% availability.

Official Microsoft modules

Microsoft Artifact Registry (MAR) is the new name of the former Microsoft Container Registry (mcr.microsoft.com). It has been the place where Microsoft publishes their official container images for some time now. The name change emphasizes that it now hosts more than just container images, now PSResources is available there!

PSResource search in Microsoft Artifact Registry
PSResource search in Microsoft Artifact Registry

To install modules from MAR you first register the repo and then use the -Repository parameter with the *-PSResource cmdlets.

Register-PSResourceRepository -Name 'mar' -Uri 'https://mcr.microsoft.com' -Trusted:$true
Find-PSResource -Repository 'mar' -Name 'az.ssh'
Name   Version Prerelease Repository Description
----   ------- ---------- ---------- -----------
Az.Ssh 0.2.3              mar        Microsoft Azure PowerShell - cmdlets for connecting to Azure VMs usi…
Install-PSResource -Name 'Az.Ssh' -Repository 'mar'
Get-Module -ListAvailable -Name 'Az.Ssh'
    Directory: C:\Users\eskil\Documents\PowerShell\Modules

ModuleType Version     Name        PSEdition ExportedCommands
---------- -------     ----        --------- ----------------
Script     0.2.3       Az.Ssh      Core,Desk {Enter-AzVM, Export-AzSshConfig}

Private Azure Container Registry

The MAR is a great source for official Microsoft modules, but for non-microsoft modules we still have to rely on the PS Gallery. With PSResourceGet we have the option to host our desired modules in our own Azure Container Registry!

Once we have the ACR populated with modules we can use group policy to only allow our private ACR and block the PS Gallery.

Deploy

The bicep code required to deploy an ACR is minimal.

1module acr 'br/public:avm/res/container-registry/registry:0.9.1' = {
2  scope: resourceGroup('rg-pwshacr')
3  params: {
4    name: 'eulapwsh'
5    acrSku: 'Basic'
6    acrAdminUserEnabled: true
7    // cacheRules: []
8  }
9}

Register

When the ACR is created, it has to be registered as a repository on the machine that will use it. Registering the repository does not require any credentials. However, the first time you perform an operation on the registered repository, you are prompted to login, if you are not authenticated yet.

$splat = @{
    Name        = 'eulapwsh'
    Uri         = 'eulapwsh.azurecr.io'
    Priority    = 40
    Trusted     = $true
}
Register-PSResourceRepository @splat

Get-PSResourceRepository
Name      Uri                                      Trusted Priority IsAllowedByPolicy
----      ---                                      ------- -------- -----------------
eulapwsh  https://eulapwsh.azurecr.io/             True    40       True
PSGallery https://www.powershellgallery.com/api/v2 False   50       True

Publish

With the repository available I download a module from the PS Gallery and publish it to the ACR.

$splat = @{
    Name               = 'EnterprisePolicyAsCode'
    Repository         = 'PSGallery'
    Version            = '10.10.0'
    TrustRepository    = $true
}
Install-PSResource @splat

Connect-AzAccount

$mod = Get-Module -ListAvailable 'EnterprisePolicyAsCode'
Publish-PSResource -Repository 'eulapwsh' -Path $mod.ModuleBase
Module available in ACR
Module available in ACR
available tags
available tags
artifact manifest
artifact manifest

Copy to ACR

Modules stored in other OCI artifact registries can easily be copied into the private ACR.

With pwsh.

$splat = @{
    RegistryName         = 'eulapwsh'
    ResourceGroupName    = 'rg-pwshacr'
    SourceRegistryUri    = 'mcr.microsoft.com'
    SourceImage          = 'psresource/az.nginx:1.2.0'
    TargetTag            = 'az.nginx:1.2.0'
}
Connect-AzContainerRegistry -Name 'eulapwsh'
Import-AzContainerRegistryImage @splat

With azure cli.

az login
az acr login -n eulapwsh
az acr import `
    --name eulapwsh `
    --source mcr.microsoft.com/psresource/az.ssh:0.2.3 `
    --image az.ssh:0.2.3

With ORAS. See FeynmanZhou blog post and the Microsoft doc to Manage OCI Artifacts with ORAS.

az login
az acr login -n eulapwsh
oras copy mcr.microsoft.com/psresource/az.dns:1.3.0 eulapwsh.azurecr.io/az.dns:1.3.0
oras copy operation
oras copy operation

Find and install modules

With the ACR populated and registered, it is just like any other psresource repository.

Find-PSResource -Repository eulapwsh -Name *

Name                   Version Prerelease Repository Description
----                   ------- ---------- ---------- -----------
EnterprisePolicyAsCode 10.10.0            eulapwsh   Enterprise Policy as Code PowerShell Module
Profiler               4.3.0              eulapwsh   Script, ScriptBlock and module performance profiler for PowerShel…
PSDocs                 0.9.0              eulapwsh   Generate markdown from PowerShell.…
PSDocs.Azure           0.3.0              eulapwsh   Generate markdown from Azure infrastructure as code (IaC) artifac…
Az.Dns                 1.3.1              eulapwsh   Microsoft Azure PowerShell - DNS service cmdlets for Azure Resourc…
Az.Nginx               1.2.0              eulapwsh   Microsoft Azure PowerShell: Nginx cmdlets
Az.Ssh                 0.2.3              eulapwsh   Microsoft Azure PowerShell - cmdlets for connecting to Azure VMs usi…
$splat = @{
    Name               = 'EnterprisePolicyAsCode'
    Repository         = 'eulapwsh'
    Version            = '10.10.0'
    TrustRepository    = $true
}
Install-PSResource @splat

ACR Cache rules

ACR has a feature called artifact cache, this lets you cache artifacts from both public and private repositories. I have updated the bicep template with a cache rule from MAR.

module acr 'br/public:avm/res/container-registry/registry:0.9.1' = {
...
    cacheRules: [
      {
        name: 'az-compute'
        sourceRepository: 'mcr.microsoft.com/psresource/az.compute'
        targetRepository: 'az.compute'
      }
    ]
...

In the azure portal now there will be a repository named az.compute associated with the cache rule, but the repo will not have any content yet…

oras copy operation
oras copy operation

According to the limitations in the docs:

Cache only occurs after at least one image pull is complete on the available container image. For every new image available, a new image pull must be complete. Currently, artifact cache doesn’t automatically pull new tags of images when a new tag is available.

To populate the cache, I do a oras pull for each module version I need. This is the only way I have found to populate anything to the target repository of the cache rule, as it seems it is protected from any push operation.

oras copy operation
oras copy operation
oras pull operation
oras pull operation

Gotchas

Dependency checks

When publishing a module, there is some prechecks like checking that all dependencies are present in the target repository. Either bypass this with the -SkipDependenciesCheck parameter or publish the dependencies first.

$mod = Get-Module -ListAvailable 'PSDocs.Azure'
Publish-PSResource -Repository 'eulapwsh' -Path $mod.ModuleBase
Publish-PSResource: Dependency 'PSDocs' was not found in repository 'eulapwsh'.  Make sure the dependency is published to the repository before publish this module.

Verify dependencies…

(Get-Module -ListAvailable 'PSDocs.Azure').RequiredModules

ModuleType Version    PreRelease Name          ExportedCommands
---------- -------    ---------- ----          ----------------
Script     0.8.0                 PSDocs

…and publish again.

# publish with dependencies
$dependency = Get-Module -ListAvailable 'PSDocs'
Publish-PSResource -Repository 'eulapwsh' -Path $dependency.ModuleBase
Publish-PSResource -Repository 'eulapwsh' -Path $mod.ModuleBase
# or ignore dependencies
Publish-PSResource -Repository 'eulapwsh' -Path $mod.ModuleBase -SkipDependenciesCheck

Closing words

I really like being able to have my own PS Gallery with an SLA, as the public gallery is down sometimes.

I hope the acr team adds some automation for pulling the initial content from a cache rule and a way to handle new versions published in the upstream. For cleanup of old module versions I would like to see some kind of time-to-live for the cache, or delete tags not pulled in the last x days.

I will also revisit this to see if I can make a declerative way to express what modules and what versions should be stored in the ACR. ACR content as Code. With cleanup of old or unwanted module versions.

Big thanks to Anam Navied, Sydney Smith and Michael Green for their talks at this years PowerShell Conference EU and PowerShell Summit! Watch their presentations on youtube: